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 ,
@@ -78,6 +79,18 @@ const RULE_ALPHANUM = [
7879 } ,
7980] ;
8081
82+ const RULE_PORT = [
83+ {
84+ validator : ( _ : any , value : number | null ) => {
85+ if ( value == null ) return Promise . resolve ( ) ;
86+ if ( ! Number . isInteger ( value ) || value < 1 ) {
87+ return Promise . reject ( "Port must be an integer greater or equal to 1." ) ;
88+ }
89+ return Promise . resolve ( ) ;
90+ } ,
91+ } ,
92+ ] ;
93+
8194// convert the configuration from the DB to fields for the table
8295function raw2configs ( raw : { [ name : string ] : Config } ) : Config [ ] {
8396 const ret : Config [ ] = [ ] ;
@@ -96,11 +109,15 @@ function raw2configs(raw: { [name: string]: Config }): Config[] {
96109 v . about = `Bucket: ${ v . bucket } ` ;
97110 break ;
98111 case "sshfs" :
99- v . about = [
112+ const about_sshfs = [
100113 `User: ${ v . user } ` ,
101114 `Host: ${ v . host } ` ,
102115 `Path: ${ v . path ?? `/user/${ v . user } ` } ` ,
103- ] . join ( "\n" ) ;
116+ ] ;
117+ if ( v . port != null && v . port !== 22 ) {
118+ about_sshfs . push ( `Port: ${ v . port } ` ) ;
119+ }
120+ v . about = about_sshfs . join ( "\n" ) ;
104121 break ;
105122 default :
106123 unreachable ( v ) ;
@@ -175,6 +192,7 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
175192 user : "" ,
176193 host : "" ,
177194 path : "" ,
195+ port : 22 ,
178196 } ) ;
179197 break ;
180198 default :
@@ -308,6 +326,9 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
308326 const conf : Config = { ...record } ;
309327 conf . secret = "" ;
310328 delete conf . about ;
329+ if ( conf . type === "sshfs" && conf . port == null ) {
330+ conf . port = 22 ;
331+ }
311332 set_new_config ( conf ) ;
312333 set_form_readonly ( conf . readonly ?? READONLY_DEFAULT ) ;
313334 setEditMode ( true ) ;
@@ -424,7 +445,7 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
424445 >
425446 < div style = { { fontSize : "90%" } } >
426447 < Icon
427- name = { record . readonly ?? false ? "lock" : "lock-open" }
448+ name = { ( record . readonly ?? false ) ? "lock" : "lock-open" }
428449 /> { " " }
429450 { record . readonly ? "Read-only" : "Read/write" }
430451 </ div >
@@ -588,9 +609,16 @@ export const Datastore: React.FC<Props> = React.memo((props: Props) => {
588609 }
589610
590611 async function save_config ( values : any ) : Promise < void > {
591- values . readonly = form_readonly ;
612+ const config = { ...values , readonly : form_readonly } ;
613+ if ( "port" in config ) {
614+ if ( config . port == null || config . port === "" ) {
615+ config . port = null ;
616+ } else if ( config . port === 22 ) {
617+ config . port = null ;
618+ }
619+ }
592620 try {
593- await set ( values ) ;
621+ await set ( config ) ;
594622 } catch ( err ) {
595623 if ( err ) set_error ( err ) ;
596624 }
@@ -774,6 +802,15 @@ function NewSSHFS({
774802 >
775803 < Input placeholder = "login.server.edu" />
776804 </ Form . Item >
805+ < Form . Item
806+ label = "Port"
807+ name = "port"
808+ rules = { RULE_PORT }
809+ tooltip = "The SSH port, defaults to 22"
810+ help = "Leave empty to use port 22."
811+ >
812+ < InputNumber min = { 1 } placeholder = "22" style = { { width : "100%" } } />
813+ </ Form . Item >
777814 < Form . Item
778815 label = "Remote Path"
779816 name = "path"
0 commit comments