Skip to content

Commit 50c62ec

Browse files
committed
Merge remote-tracking branch 'origin/datastore-sshfs-port' into pr-20251208
2 parents b27ee73 + 1301533 commit 50c62ec

File tree

5 files changed

+102
-19
lines changed

5 files changed

+102
-19
lines changed

src/packages/database/postgres/project-queries.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
/*
2-
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
2+
* This file is part of CoCalc: Copyright © 2020 – 2025 Sagemath, Inc.
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6+
import debug from "debug";
67
import { omit } from "lodash";
7-
import { PostgreSQL } from "./types";
8+
89
import { callback2 } from "@cocalc/util/async-utils";
10+
import {
11+
DUMMY_SECRET,
12+
PORT_MAX,
13+
PORT_MIN,
14+
validatePortNumber,
15+
} from "@cocalc/util/consts";
16+
import { DatastoreConfig } from "@cocalc/util/types";
917
import { query } from "./query";
10-
import debug from "debug";
18+
import { PostgreSQL } from "./types";
19+
1120
const L = debug("hub:project-queries");
12-
import { DUMMY_SECRET } from "@cocalc/util/consts";
13-
import { DatastoreConfig } from "@cocalc/util/types";
1421

1522
export async function project_has_network_access(
1623
db: PostgreSQL,
17-
project_id: string
24+
project_id: string,
1825
): Promise<boolean> {
1926
let x;
2027
try {
@@ -52,7 +59,7 @@ interface GetDSOpts {
5259
}
5360

5461
async function get_datastore(
55-
opts: GetDSOpts
62+
opts: GetDSOpts,
5663
): Promise<{ [key: string]: DatastoreConfig }> {
5764
const { db, account_id, project_id } = opts;
5865
const q: { users: any; addons?: any } = await query({
@@ -73,19 +80,34 @@ export async function project_datastore_set(
7380
db: PostgreSQL,
7481
account_id: string,
7582
project_id: string,
76-
config: any
83+
config: any,
7784
): Promise<void> {
7885
// L("project_datastore_set", config);
7986

8087
if (config.name == null) throw Error("configuration 'name' is not defined");
8188
if (typeof config.type !== "string")
8289
throw Error(
83-
"configuration 'type' is not defined (must be 'gcs', 'sshfs', ...)"
90+
"configuration 'type' is not defined (must be 'gcs', 'sshfs', ...)",
8491
);
8592

8693
// check data from user
8794
for (const [key, val] of Object.entries(config)) {
88-
if (typeof val !== "string" && typeof val !== "boolean") {
95+
if (val == null) continue;
96+
if (key === "port") {
97+
const port = validatePortNumber(val);
98+
if (port == null) {
99+
throw new Error(
100+
`Invalid value -- 'port' must be an integer between ${PORT_MIN} and ${PORT_MAX}`,
101+
);
102+
}
103+
config.port = port;
104+
continue;
105+
}
106+
if (
107+
typeof val !== "string" &&
108+
typeof val !== "boolean" &&
109+
typeof val !== "number"
110+
) {
89111
throw new Error(`Invalid value -- '${key}' is not a valid type`);
90112
}
91113
if (typeof val === "string" && val.length > 100000) {
@@ -128,7 +150,7 @@ export async function project_datastore_del(
128150
db: PostgreSQL,
129151
account_id: string,
130152
project_id: string,
131-
name: string
153+
name: string,
132154
): Promise<void> {
133155
L("project_datastore_del", name);
134156
if (typeof name !== "string" || name.length == 0) {
@@ -149,7 +171,7 @@ export async function project_datastore_del(
149171
export async function project_datastore_get(
150172
db: PostgreSQL,
151173
account_id: string,
152-
project_id: string
174+
project_id: string,
153175
): Promise<any> {
154176
try {
155177
const ds = await get_datastore({

src/packages/frontend/project/settings/datastore.tsx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* This file is part of CoCalc: Copyright © 2021 – 2023 Sagemath, Inc.
2+
* This file is part of CoCalc: Copyright © 2021 – 2025 Sagemath, Inc.
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

@@ -21,6 +21,7 @@ import {
2121
Checkbox,
2222
Form,
2323
Input,
24+
InputNumber,
2425
Modal,
2526
Popconfirm,
2627
Space,
@@ -50,6 +51,7 @@ import Password, {
5051
} from "@cocalc/frontend/components/password";
5152
import { labels } from "@cocalc/frontend/i18n";
5253
import { webapp_client } from "@cocalc/frontend/webapp-client";
54+
import { PORT_MAX, PORT_MIN, validatePortNumber } from "@cocalc/util/consts";
5355
import { DOC_CLOUD_STORAGE_URL } from "@cocalc/util/consts/project";
5456
import { DATASTORE_TITLE } from "@cocalc/util/db-schema/site-defaults";
5557
import { unreachable } from "@cocalc/util/misc";
@@ -78,6 +80,21 @@ const RULE_ALPHANUM = [
7880
},
7981
];
8082

83+
const RULE_PORT = [
84+
{
85+
validator: (_: any, value: number | null) => {
86+
if (value == null) return Promise.resolve();
87+
const port = validatePortNumber(value);
88+
if (port == null) {
89+
return Promise.reject(
90+
`Port must be an integer between ${PORT_MIN} and ${PORT_MAX}.`,
91+
);
92+
}
93+
return Promise.resolve();
94+
},
95+
},
96+
];
97+
8198
// convert the configuration from the DB to fields for the table
8299
function raw2configs(raw: { [name: string]: Config }): Config[] {
83100
const ret: Config[] = [];
@@ -96,11 +113,15 @@ function raw2configs(raw: { [name: string]: Config }): Config[] {
96113
v.about = `Bucket: ${v.bucket}`;
97114
break;
98115
case "sshfs":
99-
v.about = [
116+
const about_sshfs = [
100117
`User: ${v.user}`,
101118
`Host: ${v.host}`,
102119
`Path: ${v.path ?? `/user/${v.user}`}`,
103-
].join("\n");
120+
];
121+
if (v.port != null && v.port !== 22) {
122+
about_sshfs.push(`Port: ${v.port}`);
123+
}
124+
v.about = about_sshfs.join("\n");
104125
break;
105126
default:
106127
unreachable(v);
@@ -175,6 +196,7 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
175196
user: "",
176197
host: "",
177198
path: "",
199+
port: 22,
178200
});
179201
break;
180202
default:
@@ -308,6 +330,9 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
308330
const conf: Config = { ...record };
309331
conf.secret = "";
310332
delete conf.about;
333+
if (conf.type === "sshfs" && conf.port == null) {
334+
conf.port = 22;
335+
}
311336
set_new_config(conf);
312337
set_form_readonly(conf.readonly ?? READONLY_DEFAULT);
313338
setEditMode(true);
@@ -424,7 +449,7 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
424449
>
425450
<div style={{ fontSize: "90%" }}>
426451
<Icon
427-
name={record.readonly ?? false ? "lock" : "lock-open"}
452+
name={(record.readonly ?? false) ? "lock" : "lock-open"}
428453
/>{" "}
429454
{record.readonly ? "Read-only" : "Read/write"}
430455
</div>
@@ -588,9 +613,13 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
588613
}
589614

590615
async function save_config(values: any): Promise<void> {
591-
values.readonly = form_readonly;
616+
const config = { ...values, readonly: form_readonly };
617+
if ("port" in config) {
618+
const port = validatePortNumber(config.port);
619+
config.port = port ?? 22;
620+
}
592621
try {
593-
await set(values);
622+
await set(config);
594623
} catch (err) {
595624
if (err) set_error(err);
596625
}
@@ -774,6 +803,15 @@ function NewSSHFS({
774803
>
775804
<Input placeholder="login.server.edu" />
776805
</Form.Item>
806+
<Form.Item
807+
label="Port"
808+
name="port"
809+
rules={RULE_PORT}
810+
tooltip="The SSH port, defaults to 22"
811+
help="Leave empty to use port 22."
812+
>
813+
<InputNumber min={1} placeholder="22" style={{ width: "100%" }} />
814+
</Form.Item>
777815
<Form.Item
778816
label="Remote Path"
779817
name="path"

src/packages/util/consts/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
2+
* This file is part of CoCalc: Copyright © 2020 – 2025 Sagemath, Inc.
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

@@ -16,3 +16,5 @@ export { DUMMY_SECRET } from "./project";
1616
export { SERVER_SETTINGS_ENV_PREFIX } from "./server_settings";
1717

1818
export { ANALYTICS_COOKIE_NAME } from "./tracking";
19+
20+
export { PORT_MAX, PORT_MIN, validatePortNumber } from "./portnumber";
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.
3+
* License: MS-RSL – see LICENSE.md for details
4+
*/
5+
6+
export const PORT_MIN = 1;
7+
export const PORT_MAX = 65535;
8+
9+
export function validatePortNumber(port: unknown): number | undefined {
10+
if (port == null || port === "") return;
11+
const value =
12+
typeof port === "number"
13+
? port
14+
: typeof port === "string"
15+
? Number(port)
16+
: NaN;
17+
if (!Number.isInteger(value)) return;
18+
if (value < PORT_MIN || value > PORT_MAX) return;
19+
return value;
20+
}

src/packages/util/types/datastore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface ConfigSSHFS extends ConfigCommon {
2626
user: string;
2727
host: string;
2828
path?: string; // remote path, defaults to /home/user
29+
port?: number;
2930
}
3031

3132
export type DatastoreConfig = ConfigS3 | ConfigGCS | ConfigSSHFS;

0 commit comments

Comments
 (0)