@brftech/filex-core 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -4
- package/dist/filex-core.js +10626 -6501
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +94 -63
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +1026 -7
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/FileExplorer.vue +103 -68
- package/src/components/ConnectionGuideView.vue +333 -0
- package/src/components/ConnectionsPanel.vue +912 -0
- package/src/components/NFSExportsPanel.vue +283 -0
- package/src/components/S3KeysPanel.vue +381 -0
- package/src/components/SSHKeysPanel.vue +222 -0
- package/src/components/StorageFields.vue +362 -0
- package/src/components/TokensPanel.vue +191 -0
- package/src/components/UploadProgress.vue +5 -1
- package/src/composables/useConnections.ts +271 -0
- package/src/composables/useFileApi.ts +15 -2
- package/src/composables/useNFSExports.ts +148 -0
- package/src/composables/useS3Keys.ts +175 -0
- package/src/composables/useSSHKeys.ts +119 -0
- package/src/composables/useThumbs.ts +1 -1
- package/src/composables/useTokens.ts +121 -0
- package/src/composables/useUploadChunked.ts +433 -164
- package/src/index.ts +77 -2
- package/src/lib/connectionGuides.ts +1279 -0
- package/src/lib/realtime.ts +1 -1
- package/src/lib/uploadResume.ts +157 -0
- package/src/locales/en.ts +413 -0
- package/src/locales/tr.ts +416 -0
- package/src/modals/ConvertModal.vue +1 -1
- package/src/styles/base.css +12 -12
- package/src/types/Connections.ts +122 -0
- package/src/types/ExplorerConfig.ts +23 -2
- package/src/types/NFSExports.ts +47 -0
- package/src/types/S3Keys.ts +55 -0
- package/src/types/SSHKeys.ts +54 -0
- package/src/types/Tokens.ts +39 -0
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connectionGuides — "how do I connect to this thing?", generated from the
|
|
3
|
+
* live deployment instead of written as prose.
|
|
4
|
+
*
|
|
5
|
+
* A published document says `https://fm.example.com/dav/` and leaves the
|
|
6
|
+
* reader to substitute three values, one of which (their own username) they
|
|
7
|
+
* usually get wrong. What ships here is the real host, the real storage
|
|
8
|
+
* name and the caller's own credential, with a copy button — which is the
|
|
9
|
+
* difference between a doc and a working paste. No surveyed product
|
|
10
|
+
* (Backblaze, R2, Storj, MinIO, Garage) does this; it is the cheapest win
|
|
11
|
+
* on the whole surface.
|
|
12
|
+
*
|
|
13
|
+
* Shape of the thing: a protocol is a `GuideBuilder` registered in
|
|
14
|
+
* `GUIDE_BUILDERS`. WebDAV is built here because WebDAV is what filex
|
|
15
|
+
* serves today; S3 and SFTP are a builder each and a registry line when
|
|
16
|
+
* their servers land — no second implementation, no second UI.
|
|
17
|
+
*
|
|
18
|
+
* The commands are not invented. They are the ones verified on 2026-08-14
|
|
19
|
+
* and written down in `docs/WEBDAV.md` and §13.5 of the write-path
|
|
20
|
+
* handover, including the three Windows registry limits that otherwise
|
|
21
|
+
* look exactly like filex bugs.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type Translate = (key: string, vars?: Record<string, string | number>) => string;
|
|
25
|
+
|
|
26
|
+
/** Everything a guide is allowed to know about the deployment. */
|
|
27
|
+
export interface GuideContext {
|
|
28
|
+
/** Absolute origin of the server, e.g. `https://fm.example.com`. */
|
|
29
|
+
origin: string;
|
|
30
|
+
/** The caller's own account e-mail — the username on every protocol. */
|
|
31
|
+
user: string;
|
|
32
|
+
/** Storage names the caller may see. */
|
|
33
|
+
storages: string[];
|
|
34
|
+
/** The storage the page is currently focused on, when any. */
|
|
35
|
+
storage?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The S3 endpoint, as the server computes it.
|
|
38
|
+
*
|
|
39
|
+
* ⚠ NOT derived from `origin` here. With a dedicated host the endpoint is
|
|
40
|
+
* that host's root; without one it lives under `/s3`, and a client pointed
|
|
41
|
+
* at the application root reaches the web app — rclone reported
|
|
42
|
+
* "XML syntax error on line 10" against an HTML redirect page. The server
|
|
43
|
+
* returns this with every key, and the guide repeats it verbatim.
|
|
44
|
+
*/
|
|
45
|
+
s3Endpoint?: string;
|
|
46
|
+
/** True when clients must be told to force path-style addressing. */
|
|
47
|
+
s3PathStyle?: boolean;
|
|
48
|
+
/** The access key id to write into the examples, when the caller has one. */
|
|
49
|
+
s3AccessKeyID?: string;
|
|
50
|
+
/** The freshly minted secret, shown only while the user is looking at it. */
|
|
51
|
+
s3Secret?: string;
|
|
52
|
+
/** The SFTP endpoint, as the server reports it. */
|
|
53
|
+
sftpHost?: string;
|
|
54
|
+
sftpPort?: number;
|
|
55
|
+
/** False when the operator has the endpoint switched off. */
|
|
56
|
+
sftpEnabled?: boolean;
|
|
57
|
+
/** The login name a client must use — the username when there is one. */
|
|
58
|
+
sftpLogin?: string;
|
|
59
|
+
/** True when the account has at least one usable key registered. */
|
|
60
|
+
sftpHasKey?: boolean;
|
|
61
|
+
/** The FTPS endpoint, as the server reports it. */
|
|
62
|
+
ftpsHost?: string;
|
|
63
|
+
ftpsPort?: number;
|
|
64
|
+
ftpsEnabled?: boolean;
|
|
65
|
+
/** The passive data-port range, which the client's firewall must allow. */
|
|
66
|
+
ftpsPasvMin?: number;
|
|
67
|
+
ftpsPasvMax?: number;
|
|
68
|
+
/** True when the server is using a self-signed certificate. */
|
|
69
|
+
ftpsSelfSigned?: boolean;
|
|
70
|
+
/** The NFS endpoint, as the server reports it. */
|
|
71
|
+
nfsHost?: string;
|
|
72
|
+
nfsPort?: number;
|
|
73
|
+
nfsEnabled?: boolean;
|
|
74
|
+
/** The freshly minted export path — the credential, shown once. */
|
|
75
|
+
nfsPath?: string;
|
|
76
|
+
/** True when the active export refuses writes. */
|
|
77
|
+
nfsReadOnly?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type GuideBlockKind = 'text' | 'steps' | 'code' | 'note' | 'warn';
|
|
81
|
+
|
|
82
|
+
export interface GuideBlock {
|
|
83
|
+
kind: GuideBlockKind;
|
|
84
|
+
/** Prose for `text` / `note` / `warn`. */
|
|
85
|
+
text?: string;
|
|
86
|
+
/** Ordered instructions for `steps`. */
|
|
87
|
+
steps?: string[];
|
|
88
|
+
/** Copyable payload for `code`. */
|
|
89
|
+
code?: string;
|
|
90
|
+
/** Syntax hint / file name shown above a code block. */
|
|
91
|
+
caption?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface GuideClient {
|
|
95
|
+
id: string;
|
|
96
|
+
/** Product name — never translated ("Cyberduck" is Cyberduck). */
|
|
97
|
+
name: string;
|
|
98
|
+
platform: 'windows' | 'macos' | 'linux' | 'any';
|
|
99
|
+
blocks: GuideBlock[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One connection fact, rendered as a copyable row. */
|
|
103
|
+
export interface GuideFact {
|
|
104
|
+
label: string;
|
|
105
|
+
value: string;
|
|
106
|
+
hint?: string;
|
|
107
|
+
/** Rendered as a hint rather than a value — filex never has the
|
|
108
|
+
* plaintext of your password and must not pretend otherwise. */
|
|
109
|
+
placeholderOnly?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ProtocolGuide {
|
|
113
|
+
id: string;
|
|
114
|
+
/** Protocol name — a wire protocol is not translated either. */
|
|
115
|
+
name: string;
|
|
116
|
+
summary: string;
|
|
117
|
+
/** Server + credential, filled in from this deployment. */
|
|
118
|
+
facts: GuideFact[];
|
|
119
|
+
clients: GuideClient[];
|
|
120
|
+
notes: GuideBlock[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type GuideBuilder = (ctx: GuideContext, t: Translate) => ProtocolGuide;
|
|
124
|
+
|
|
125
|
+
/** `https://fm.example.com` → `fm.example.com`; anything unparseable comes back whole. */
|
|
126
|
+
export function hostOf(origin: string): string {
|
|
127
|
+
try {
|
|
128
|
+
return new URL(origin).host;
|
|
129
|
+
} catch {
|
|
130
|
+
return origin.replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** True when the deployment is not on TLS — several clients refuse that,
|
|
135
|
+
* and Windows refuses it silently, which is worse. */
|
|
136
|
+
export function isPlainHttp(origin: string): boolean {
|
|
137
|
+
return /^http:\/\//i.test(origin);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The username a client should be given. Falls back to a placeholder so a
|
|
142
|
+
* guide rendered before `/api/auth/me` answers is still readable rather
|
|
143
|
+
* than showing `undefined` in the middle of a command line.
|
|
144
|
+
*/
|
|
145
|
+
function userOf(ctx: GuideContext, t: Translate): string {
|
|
146
|
+
return ctx.user || t('conn.guide.userPlaceholder');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
150
|
+
// WebDAV
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export const buildWebdavGuide: GuideBuilder = (ctx, t) => {
|
|
154
|
+
const origin = ctx.origin.replace(/\/+$/, '');
|
|
155
|
+
const root = `${origin}/dav/`;
|
|
156
|
+
const target = ctx.storage ? `${origin}/dav/${ctx.storage}/` : root;
|
|
157
|
+
const user = userOf(ctx, t);
|
|
158
|
+
const secret = t('conn.guide.secretPlaceholder');
|
|
159
|
+
|
|
160
|
+
const facts: GuideFact[] = [
|
|
161
|
+
{ label: t('conn.guide.fact.url'), value: target, hint: ctx.storage ? t('conn.guide.fact.urlStorageHint', { storage: ctx.storage }) : t('conn.guide.fact.urlRootHint') },
|
|
162
|
+
{ label: t('conn.guide.fact.user'), value: user, hint: t('conn.guide.webdav.userHint') },
|
|
163
|
+
{ label: t('conn.guide.fact.password'), value: secret, hint: t('conn.guide.webdav.passwordHint'), placeholderOnly: true },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const clients: GuideClient[] = [
|
|
167
|
+
{
|
|
168
|
+
id: 'windows',
|
|
169
|
+
name: 'Windows Explorer',
|
|
170
|
+
platform: 'windows',
|
|
171
|
+
blocks: [
|
|
172
|
+
{
|
|
173
|
+
kind: 'steps',
|
|
174
|
+
steps: [
|
|
175
|
+
t('conn.guide.webdav.win.s1'),
|
|
176
|
+
t('conn.guide.webdav.win.s2', { url: target }),
|
|
177
|
+
t('conn.guide.webdav.win.s3'),
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
kind: 'code',
|
|
182
|
+
caption: t('conn.guide.webdav.win.cmdCaption'),
|
|
183
|
+
code: `net use Z: "${target}" /user:${user} ${secret} /persistent:yes`,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
kind: 'warn',
|
|
187
|
+
text: t('conn.guide.webdav.win.limits'),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
kind: 'code',
|
|
191
|
+
caption: t('conn.guide.webdav.win.regCaption'),
|
|
192
|
+
code: [
|
|
193
|
+
'reg add "HKLM\\SYSTEM\\CurrentControlSet\\Services\\WebClient\\Parameters" /v FileSizeLimitInBytes /t REG_DWORD /d 4294967295 /f',
|
|
194
|
+
'reg add "HKLM\\SYSTEM\\CurrentControlSet\\Services\\WebClient\\Parameters" /v FileAttributesLimitInBytes /t REG_DWORD /d 20000000 /f',
|
|
195
|
+
'net stop webclient && net start webclient',
|
|
196
|
+
].join('\n'),
|
|
197
|
+
},
|
|
198
|
+
{ kind: 'note', text: t('conn.guide.webdav.win.https') },
|
|
199
|
+
{ kind: 'note', text: t('conn.guide.webdav.win.persist') },
|
|
200
|
+
{ kind: 'note', text: t('conn.guide.webdav.win.service') },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'macos',
|
|
205
|
+
name: 'macOS Finder',
|
|
206
|
+
platform: 'macos',
|
|
207
|
+
blocks: [
|
|
208
|
+
{
|
|
209
|
+
kind: 'steps',
|
|
210
|
+
steps: [
|
|
211
|
+
t('conn.guide.webdav.mac.s1'),
|
|
212
|
+
t('conn.guide.webdav.mac.s2', { url: target }),
|
|
213
|
+
t('conn.guide.webdav.mac.s3'),
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{ kind: 'note', text: t('conn.guide.webdav.mac.note') },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'linux',
|
|
221
|
+
name: 'Linux (davfs2 / GNOME / KDE)',
|
|
222
|
+
platform: 'linux',
|
|
223
|
+
blocks: [
|
|
224
|
+
{
|
|
225
|
+
kind: 'code',
|
|
226
|
+
caption: t('conn.guide.webdav.linux.mountCaption'),
|
|
227
|
+
code: [
|
|
228
|
+
`sudo mount -t davfs ${target} /mnt/filex`,
|
|
229
|
+
'',
|
|
230
|
+
`# ${t('conn.guide.webdav.linux.gvfsComment')}`,
|
|
231
|
+
`gio mount ${target.replace(/^https:/i, 'davs:').replace(/^http:/i, 'dav:')}`,
|
|
232
|
+
].join('\n'),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
kind: 'warn',
|
|
236
|
+
text: t('conn.guide.webdav.linux.locks'),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
kind: 'code',
|
|
240
|
+
caption: '/etc/davfs2/davfs2.conf',
|
|
241
|
+
code: 'use_locks 0',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'rclone',
|
|
247
|
+
name: 'rclone',
|
|
248
|
+
platform: 'any',
|
|
249
|
+
blocks: [
|
|
250
|
+
{
|
|
251
|
+
kind: 'code',
|
|
252
|
+
caption: t('conn.guide.webdav.rclone.obscureCaption'),
|
|
253
|
+
code: `rclone obscure "${secret}"`,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
kind: 'code',
|
|
257
|
+
caption: '~/.config/rclone/rclone.conf',
|
|
258
|
+
code: [
|
|
259
|
+
'[filex]',
|
|
260
|
+
'type = webdav',
|
|
261
|
+
`url = ${origin}/dav`,
|
|
262
|
+
'vendor = other',
|
|
263
|
+
`user = ${user}`,
|
|
264
|
+
`pass = ${t('conn.guide.webdav.rclone.passPlaceholder')}`,
|
|
265
|
+
].join('\n'),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
kind: 'code',
|
|
269
|
+
caption: t('conn.guide.webdav.rclone.useCaption'),
|
|
270
|
+
code: [
|
|
271
|
+
'rclone lsd filex:',
|
|
272
|
+
ctx.storage ? `rclone lsl filex:${ctx.storage}` : 'rclone lsl filex:<storage>',
|
|
273
|
+
ctx.storage
|
|
274
|
+
? `rclone copy ./local filex:${ctx.storage}/backup`
|
|
275
|
+
: 'rclone copy ./local filex:<storage>/backup',
|
|
276
|
+
'rclone mount filex: /mnt/filex',
|
|
277
|
+
].join('\n'),
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'cyberduck',
|
|
283
|
+
name: 'Cyberduck / Mountain Duck',
|
|
284
|
+
platform: 'any',
|
|
285
|
+
blocks: [
|
|
286
|
+
{
|
|
287
|
+
kind: 'steps',
|
|
288
|
+
steps: [
|
|
289
|
+
t('conn.guide.webdav.duck.s1'),
|
|
290
|
+
t('conn.guide.webdav.duck.s2', { host: hostOf(origin) }),
|
|
291
|
+
t('conn.guide.webdav.duck.s3', { path: ctx.storage ? `/dav/${ctx.storage}/` : '/dav/' }),
|
|
292
|
+
t('conn.guide.webdav.duck.s4', { user }),
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const notes: GuideBlock[] = [
|
|
300
|
+
{ kind: 'note', text: t('conn.guide.webdav.note.delete') },
|
|
301
|
+
{ kind: 'note', text: t('conn.guide.webdav.note.locks') },
|
|
302
|
+
{ kind: 'note', text: t('conn.guide.webdav.note.permissions') },
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
if (isPlainHttp(origin)) {
|
|
306
|
+
// Not decoration: over plain HTTP Windows refuses to send Basic
|
|
307
|
+
// credentials at all, and says nothing useful about why.
|
|
308
|
+
notes.unshift({ kind: 'warn', text: t('conn.guide.webdav.note.http') });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
id: 'webdav',
|
|
313
|
+
name: 'WebDAV',
|
|
314
|
+
summary: t('conn.guide.webdav.summary'),
|
|
315
|
+
facts,
|
|
316
|
+
clients,
|
|
317
|
+
notes,
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
322
|
+
// S3
|
|
323
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Every command here was RUN against the endpoint on 2026-08-16, not copied
|
|
327
|
+
* from a vendor page: rclone 1.73.5, aws-cli 2.11.6, restic 0.19.1, mc
|
|
328
|
+
* 2025-08-13 and s3fs 1.93 each did a full round trip, and the flags below are
|
|
329
|
+
* the ones those runs actually needed. Five of the endpoint's bugs were found
|
|
330
|
+
* that way — a guide assembled from documentation would have shipped them.
|
|
331
|
+
*/
|
|
332
|
+
export const buildS3Guide: GuideBuilder = (ctx, t) => {
|
|
333
|
+
const endpoint = (ctx.s3Endpoint || `${ctx.origin.replace(/\/+$/, '')}/s3`).replace(/\/+$/, '');
|
|
334
|
+
const bucket = ctx.storage || ctx.storages[0] || '<bucket>';
|
|
335
|
+
const akid = ctx.s3AccessKeyID || t('conn.guide.s3.keyPlaceholder');
|
|
336
|
+
const secret = ctx.s3Secret || t('conn.guide.s3.secretPlaceholder');
|
|
337
|
+
const pathStyle = ctx.s3PathStyle !== false;
|
|
338
|
+
const host = hostOf(endpoint);
|
|
339
|
+
|
|
340
|
+
const facts: GuideFact[] = [
|
|
341
|
+
{ label: t('conn.guide.s3.fact.endpoint'), value: endpoint, hint: t('conn.guide.s3.fact.endpointHint') },
|
|
342
|
+
{ label: t('conn.guide.s3.fact.bucket'), value: bucket, hint: t('conn.guide.s3.fact.bucketHint') },
|
|
343
|
+
{ label: t('conn.guide.s3.fact.key'), value: akid, hint: t('conn.guide.s3.fact.keyHint') },
|
|
344
|
+
{
|
|
345
|
+
label: t('conn.guide.s3.fact.secret'),
|
|
346
|
+
value: secret,
|
|
347
|
+
hint: t('conn.guide.s3.fact.secretHint'),
|
|
348
|
+
// ⚠ Only real right after minting. filex stores the secret sealed and
|
|
349
|
+
// cannot show it again, and a guide that printed something secret-shaped
|
|
350
|
+
// would be teaching a value that authenticates as nothing.
|
|
351
|
+
placeholderOnly: !ctx.s3Secret,
|
|
352
|
+
},
|
|
353
|
+
{ label: t('conn.guide.s3.fact.region'), value: 'us-east-1', hint: t('conn.guide.s3.fact.regionHint') },
|
|
354
|
+
{
|
|
355
|
+
label: t('conn.guide.s3.fact.addressing'),
|
|
356
|
+
value: pathStyle ? t('conn.guide.s3.fact.pathStyle') : t('conn.guide.s3.fact.virtualHosted'),
|
|
357
|
+
hint: pathStyle ? t('conn.guide.s3.fact.pathStyleHint') : t('conn.guide.s3.fact.virtualHostedHint'),
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const clients: GuideClient[] = [
|
|
362
|
+
{
|
|
363
|
+
id: 'rclone',
|
|
364
|
+
name: 'rclone',
|
|
365
|
+
platform: 'any',
|
|
366
|
+
blocks: [
|
|
367
|
+
{
|
|
368
|
+
kind: 'code',
|
|
369
|
+
caption: '~/.config/rclone/rclone.conf',
|
|
370
|
+
code: [
|
|
371
|
+
'[filex]',
|
|
372
|
+
'type = s3',
|
|
373
|
+
'provider = Other',
|
|
374
|
+
`endpoint = ${endpoint}`,
|
|
375
|
+
`access_key_id = ${akid}`,
|
|
376
|
+
`secret_access_key = ${secret}`,
|
|
377
|
+
'region = us-east-1',
|
|
378
|
+
...(pathStyle ? ['force_path_style = true'] : []),
|
|
379
|
+
].join('\n'),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
kind: 'code',
|
|
383
|
+
caption: t('conn.guide.s3.rclone.useCaption'),
|
|
384
|
+
code: [
|
|
385
|
+
'rclone lsd filex:',
|
|
386
|
+
`rclone copy ./local filex:${bucket}/backup -P`,
|
|
387
|
+
`rclone sync ./local filex:${bucket}/backup`,
|
|
388
|
+
`rclone mount filex:${bucket} /mnt/filex`,
|
|
389
|
+
].join('\n'),
|
|
390
|
+
},
|
|
391
|
+
{ kind: 'note', text: t('conn.guide.s3.rclone.mtime') },
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 'awscli',
|
|
396
|
+
name: 'AWS CLI',
|
|
397
|
+
platform: 'any',
|
|
398
|
+
blocks: [
|
|
399
|
+
{
|
|
400
|
+
kind: 'code',
|
|
401
|
+
caption: t('conn.guide.s3.aws.configureCaption'),
|
|
402
|
+
code: [
|
|
403
|
+
'aws configure set aws_access_key_id ' + akid,
|
|
404
|
+
'aws configure set aws_secret_access_key ' + secret,
|
|
405
|
+
'aws configure set region us-east-1',
|
|
406
|
+
].join('\n'),
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
kind: 'code',
|
|
410
|
+
caption: t('conn.guide.s3.aws.useCaption'),
|
|
411
|
+
code: [
|
|
412
|
+
`aws --endpoint-url ${endpoint} s3 ls`,
|
|
413
|
+
`aws --endpoint-url ${endpoint} s3 cp ./file.pdf s3://${bucket}/file.pdf`,
|
|
414
|
+
`aws --endpoint-url ${endpoint} s3 sync ./local s3://${bucket}/backup`,
|
|
415
|
+
`aws --endpoint-url ${endpoint} s3 presign s3://${bucket}/file.pdf --expires-in 300`,
|
|
416
|
+
].join('\n'),
|
|
417
|
+
},
|
|
418
|
+
...(pathStyle
|
|
419
|
+
? [
|
|
420
|
+
{
|
|
421
|
+
kind: 'note' as GuideBlockKind,
|
|
422
|
+
text: t('conn.guide.s3.aws.pathStyle'),
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
kind: 'code' as GuideBlockKind,
|
|
426
|
+
caption: '~/.aws/config',
|
|
427
|
+
code: ['[default]', 's3 =', ' addressing_style = path'].join('\n'),
|
|
428
|
+
},
|
|
429
|
+
]
|
|
430
|
+
: []),
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
id: 'restic',
|
|
435
|
+
name: 'restic',
|
|
436
|
+
platform: 'any',
|
|
437
|
+
blocks: [
|
|
438
|
+
{
|
|
439
|
+
kind: 'code',
|
|
440
|
+
caption: t('conn.guide.s3.restic.envCaption'),
|
|
441
|
+
code: [
|
|
442
|
+
`export AWS_ACCESS_KEY_ID=${akid}`,
|
|
443
|
+
`export AWS_SECRET_ACCESS_KEY=${secret}`,
|
|
444
|
+
`export RESTIC_REPOSITORY="s3:${endpoint}/${bucket}/restic"`,
|
|
445
|
+
'export RESTIC_PASSWORD=…',
|
|
446
|
+
].join('\n'),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
kind: 'code',
|
|
450
|
+
caption: t('conn.guide.s3.restic.useCaption'),
|
|
451
|
+
code: [
|
|
452
|
+
`restic init${pathStyle ? ' -o s3.bucket-lookup=path' : ''}`,
|
|
453
|
+
`restic backup ~/Documents${pathStyle ? ' -o s3.bucket-lookup=path' : ''}`,
|
|
454
|
+
`restic check --read-data${pathStyle ? ' -o s3.bucket-lookup=path' : ''}`,
|
|
455
|
+
`restic forget --keep-daily 7 --prune${pathStyle ? ' -o s3.bucket-lookup=path' : ''}`,
|
|
456
|
+
].join('\n'),
|
|
457
|
+
},
|
|
458
|
+
{ kind: 'note', text: t('conn.guide.s3.restic.verified') },
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: 'mc',
|
|
463
|
+
name: 'MinIO Client (mc)',
|
|
464
|
+
platform: 'any',
|
|
465
|
+
blocks: [
|
|
466
|
+
{
|
|
467
|
+
kind: 'code',
|
|
468
|
+
caption: t('conn.guide.s3.mc.aliasCaption'),
|
|
469
|
+
code: `mc alias set filex ${endpoint} ${akid} ${secret} --api S3v4${pathStyle ? ' --path on' : ''}`,
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
kind: 'code',
|
|
473
|
+
caption: t('conn.guide.s3.mc.useCaption'),
|
|
474
|
+
code: [
|
|
475
|
+
'mc ls filex',
|
|
476
|
+
`mc cp ./file.pdf filex/${bucket}/file.pdf`,
|
|
477
|
+
`mc mirror ./local filex/${bucket}/backup`,
|
|
478
|
+
`mc du filex/${bucket}`,
|
|
479
|
+
].join('\n'),
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
id: 's3fs',
|
|
485
|
+
name: 's3fs (Linux / macOS)',
|
|
486
|
+
platform: 'linux',
|
|
487
|
+
blocks: [
|
|
488
|
+
{
|
|
489
|
+
kind: 'code',
|
|
490
|
+
caption: t('conn.guide.s3.s3fs.credsCaption'),
|
|
491
|
+
code: [
|
|
492
|
+
`echo "${akid}:${secret}" > ~/.passwd-s3fs`,
|
|
493
|
+
'chmod 600 ~/.passwd-s3fs',
|
|
494
|
+
].join('\n'),
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
kind: 'code',
|
|
498
|
+
caption: t('conn.guide.s3.s3fs.mountCaption'),
|
|
499
|
+
code: [
|
|
500
|
+
`s3fs ${bucket} /mnt/filex \\`,
|
|
501
|
+
' -o passwd_file=~/.passwd-s3fs \\',
|
|
502
|
+
` -o url=${endpoint} \\`,
|
|
503
|
+
...(pathStyle ? [' -o use_path_request_style'] : []),
|
|
504
|
+
].join('\n'),
|
|
505
|
+
},
|
|
506
|
+
{ kind: 'note', text: t('conn.guide.s3.s3fs.note') },
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: 'cyberduck',
|
|
511
|
+
name: 'Cyberduck / Mountain Duck',
|
|
512
|
+
platform: 'any',
|
|
513
|
+
blocks: [
|
|
514
|
+
{
|
|
515
|
+
kind: 'steps',
|
|
516
|
+
steps: [
|
|
517
|
+
t('conn.guide.s3.duck.s1'),
|
|
518
|
+
t('conn.guide.s3.duck.s2', { host }),
|
|
519
|
+
t('conn.guide.s3.duck.s3', { key: akid }),
|
|
520
|
+
t('conn.guide.s3.duck.s4'),
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
...(pathStyle ? [{ kind: 'warn' as GuideBlockKind, text: t('conn.guide.s3.duck.pathStyle') }] : []),
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: 'sdk',
|
|
528
|
+
name: 'SDK (Go / Python / JS)',
|
|
529
|
+
platform: 'any',
|
|
530
|
+
blocks: [
|
|
531
|
+
{
|
|
532
|
+
kind: 'code',
|
|
533
|
+
caption: 'boto3',
|
|
534
|
+
code: [
|
|
535
|
+
'import boto3',
|
|
536
|
+
's3 = boto3.client(',
|
|
537
|
+
' "s3",',
|
|
538
|
+
` endpoint_url="${endpoint}",`,
|
|
539
|
+
` aws_access_key_id="${akid}",`,
|
|
540
|
+
` aws_secret_access_key="${secret}",`,
|
|
541
|
+
' region_name="us-east-1",',
|
|
542
|
+
...(pathStyle
|
|
543
|
+
? [
|
|
544
|
+
' config=boto3.session.Config(s3={"addressing_style": "path"}),',
|
|
545
|
+
]
|
|
546
|
+
: []),
|
|
547
|
+
')',
|
|
548
|
+
`print([o["Key"] for o in s3.list_objects_v2(Bucket="${bucket}").get("Contents", [])])`,
|
|
549
|
+
].join('\n'),
|
|
550
|
+
},
|
|
551
|
+
{ kind: 'note', text: t('conn.guide.s3.sdk.note') },
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
|
|
556
|
+
const notes: GuideBlock[] = [
|
|
557
|
+
{ kind: 'note', text: t('conn.guide.s3.note.buckets') },
|
|
558
|
+
{ kind: 'note', text: t('conn.guide.s3.note.permissions') },
|
|
559
|
+
{ kind: 'note', text: t('conn.guide.s3.note.trash') },
|
|
560
|
+
{ kind: 'note', text: t('conn.guide.s3.note.mtime') },
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
if (pathStyle) {
|
|
564
|
+
// First, because it is the one setting that makes a current SDK fail with
|
|
565
|
+
// a DNS error that names neither filex nor the cause.
|
|
566
|
+
notes.unshift({ kind: 'warn', text: t('conn.guide.s3.note.pathStyle') });
|
|
567
|
+
}
|
|
568
|
+
if (isPlainHttp(endpoint)) {
|
|
569
|
+
notes.unshift({ kind: 'warn', text: t('conn.guide.s3.note.http') });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
id: 's3',
|
|
574
|
+
name: 'S3',
|
|
575
|
+
summary: t('conn.guide.s3.summary'),
|
|
576
|
+
facts,
|
|
577
|
+
clients,
|
|
578
|
+
notes,
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
584
|
+
// SFTP
|
|
585
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Every command here was RUN against the endpoint on 2026-08-16 with OpenSSH
|
|
589
|
+
* 9.6 and rclone 1.73 — including the two settings rclone needs because filex
|
|
590
|
+
* has no shell, which are the difference between a clean run and a pile of
|
|
591
|
+
* warnings about a missing `md5sum`.
|
|
592
|
+
*/
|
|
593
|
+
export const buildSftpGuide: GuideBuilder = (ctx, t) => {
|
|
594
|
+
const host = ctx.sftpHost || hostOf(ctx.origin);
|
|
595
|
+
const port = ctx.sftpPort || 2022;
|
|
596
|
+
const user = ctx.sftpLogin || userOf(ctx, t);
|
|
597
|
+
const target = ctx.storage ? `/${ctx.storage}` : '/<storage>';
|
|
598
|
+
const portFlag = port === 22 ? '' : ` -P ${port}`;
|
|
599
|
+
|
|
600
|
+
const facts: GuideFact[] = [
|
|
601
|
+
{ label: t('conn.guide.sftp.fact.host'), value: host, hint: t('conn.guide.sftp.fact.hostHint') },
|
|
602
|
+
{ label: t('conn.guide.sftp.fact.port'), value: String(port), hint: t('conn.guide.sftp.fact.portHint') },
|
|
603
|
+
{ label: t('conn.guide.fact.user'), value: user, hint: t('conn.guide.sftp.fact.userHint') },
|
|
604
|
+
{
|
|
605
|
+
label: t('conn.guide.sftp.fact.auth'),
|
|
606
|
+
value: ctx.sftpHasKey ? t('conn.guide.sftp.fact.authKey') : t('conn.guide.sftp.fact.authPassword'),
|
|
607
|
+
hint: t('conn.guide.sftp.fact.authHint'),
|
|
608
|
+
placeholderOnly: true,
|
|
609
|
+
},
|
|
610
|
+
{ label: t('conn.guide.sftp.fact.path'), value: target, hint: t('conn.guide.sftp.fact.pathHint') },
|
|
611
|
+
];
|
|
612
|
+
|
|
613
|
+
const clients: GuideClient[] = [
|
|
614
|
+
{
|
|
615
|
+
id: 'openssh',
|
|
616
|
+
name: 'OpenSSH (sftp / scp)',
|
|
617
|
+
platform: 'any',
|
|
618
|
+
blocks: [
|
|
619
|
+
{
|
|
620
|
+
kind: 'code',
|
|
621
|
+
caption: t('conn.guide.sftp.openssh.connectCaption'),
|
|
622
|
+
code: [
|
|
623
|
+
`sftp${portFlag} ${user}@${host}`,
|
|
624
|
+
`scp${portFlag} ./report.pdf ${user}@${host}:${target}/report.pdf`,
|
|
625
|
+
`scp${portFlag} ${user}@${host}:${target}/report.pdf ./`,
|
|
626
|
+
].join('\n'),
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
kind: 'code',
|
|
630
|
+
caption: '~/.ssh/config',
|
|
631
|
+
code: [
|
|
632
|
+
'Host filex',
|
|
633
|
+
` HostName ${host}`,
|
|
634
|
+
` Port ${port}`,
|
|
635
|
+
` User ${user}`,
|
|
636
|
+
' # IdentityFile ~/.ssh/id_ed25519',
|
|
637
|
+
].join('\n'),
|
|
638
|
+
},
|
|
639
|
+
{ kind: 'note', text: t('conn.guide.sftp.openssh.noShell') },
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
id: 'key',
|
|
644
|
+
name: t('conn.guide.sftp.key.tab'),
|
|
645
|
+
platform: 'any',
|
|
646
|
+
blocks: [
|
|
647
|
+
{
|
|
648
|
+
kind: 'steps',
|
|
649
|
+
steps: [
|
|
650
|
+
t('conn.guide.sftp.key.s1'),
|
|
651
|
+
t('conn.guide.sftp.key.s2'),
|
|
652
|
+
t('conn.guide.sftp.key.s3'),
|
|
653
|
+
],
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
kind: 'code',
|
|
657
|
+
caption: t('conn.guide.sftp.key.genCaption'),
|
|
658
|
+
code: ['ssh-keygen -t ed25519 -C "filex"', 'cat ~/.ssh/id_ed25519.pub'].join('\n'),
|
|
659
|
+
},
|
|
660
|
+
// ⚠ The one command people reach for, and the one that cannot work.
|
|
661
|
+
{ kind: 'warn', text: t('conn.guide.sftp.key.noCopyId') },
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: 'winscp',
|
|
666
|
+
name: 'WinSCP',
|
|
667
|
+
platform: 'windows',
|
|
668
|
+
blocks: [
|
|
669
|
+
{
|
|
670
|
+
kind: 'steps',
|
|
671
|
+
steps: [
|
|
672
|
+
t('conn.guide.sftp.winscp.s1'),
|
|
673
|
+
t('conn.guide.sftp.winscp.s2', { host, port: String(port) }),
|
|
674
|
+
t('conn.guide.sftp.winscp.s3', { user }),
|
|
675
|
+
t('conn.guide.sftp.winscp.s4'),
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
id: 'filezilla',
|
|
682
|
+
name: 'FileZilla',
|
|
683
|
+
platform: 'any',
|
|
684
|
+
blocks: [
|
|
685
|
+
{
|
|
686
|
+
kind: 'steps',
|
|
687
|
+
steps: [
|
|
688
|
+
t('conn.guide.sftp.filezilla.s1'),
|
|
689
|
+
t('conn.guide.sftp.filezilla.s2', { host: `sftp://${host}`, port: String(port) }),
|
|
690
|
+
t('conn.guide.sftp.filezilla.s3', { user }),
|
|
691
|
+
],
|
|
692
|
+
},
|
|
693
|
+
],
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
id: 'rclone',
|
|
697
|
+
name: 'rclone',
|
|
698
|
+
platform: 'any',
|
|
699
|
+
blocks: [
|
|
700
|
+
{
|
|
701
|
+
kind: 'code',
|
|
702
|
+
caption: '~/.config/rclone/rclone.conf',
|
|
703
|
+
code: [
|
|
704
|
+
'[filex-sftp]',
|
|
705
|
+
'type = sftp',
|
|
706
|
+
`host = ${host}`,
|
|
707
|
+
`port = ${port}`,
|
|
708
|
+
`user = ${user}`,
|
|
709
|
+
'key_file = ~/.ssh/id_ed25519',
|
|
710
|
+
'# filex has no shell, so tell rclone not to look for one:',
|
|
711
|
+
'shell_type = none',
|
|
712
|
+
'md5sum_command = none',
|
|
713
|
+
'sha1sum_command = none',
|
|
714
|
+
].join('\n'),
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
kind: 'code',
|
|
718
|
+
caption: t('conn.guide.sftp.rclone.useCaption'),
|
|
719
|
+
code: [
|
|
720
|
+
'rclone lsd filex-sftp:',
|
|
721
|
+
`rclone copy ./local filex-sftp:${target}/backup -P`,
|
|
722
|
+
`rclone sync ./local filex-sftp:${target}/backup`,
|
|
723
|
+
].join('\n'),
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: 'sshfs',
|
|
729
|
+
name: 'sshfs (Linux / macOS)',
|
|
730
|
+
platform: 'linux',
|
|
731
|
+
blocks: [
|
|
732
|
+
{
|
|
733
|
+
kind: 'code',
|
|
734
|
+
caption: t('conn.guide.sftp.sshfs.mountCaption'),
|
|
735
|
+
code: [
|
|
736
|
+
`sshfs -p ${port} ${user}@${host}:${target} /mnt/filex`,
|
|
737
|
+
'fusermount -u /mnt/filex',
|
|
738
|
+
].join('\n'),
|
|
739
|
+
},
|
|
740
|
+
{ kind: 'note', text: t('conn.guide.sftp.sshfs.note') },
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
];
|
|
744
|
+
|
|
745
|
+
const notes: GuideBlock[] = [
|
|
746
|
+
{ kind: 'note', text: t('conn.guide.sftp.note.storages') },
|
|
747
|
+
{ kind: 'note', text: t('conn.guide.sftp.note.permissions') },
|
|
748
|
+
{ kind: 'note', text: t('conn.guide.sftp.note.trash') },
|
|
749
|
+
{ kind: 'note', text: t('conn.guide.sftp.note.totp') },
|
|
750
|
+
];
|
|
751
|
+
if (ctx.sftpEnabled === false) {
|
|
752
|
+
notes.unshift({ kind: 'warn', text: t('conn.guide.sftp.note.disabled') });
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
id: 'sftp',
|
|
757
|
+
name: 'SFTP',
|
|
758
|
+
summary: t('conn.guide.sftp.summary'),
|
|
759
|
+
facts,
|
|
760
|
+
clients,
|
|
761
|
+
notes,
|
|
762
|
+
};
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
767
|
+
// FTPS
|
|
768
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Every command here was RUN against the endpoint on 2026-08-16 with curl
|
|
772
|
+
* 8.5, lftp 4.9 and rclone 1.73 — including the settings each of them needs to
|
|
773
|
+
* negotiate explicit TLS, which is the half of FTPS that goes wrong quietly.
|
|
774
|
+
*/
|
|
775
|
+
export const buildFtpsGuide: GuideBuilder = (ctx, t) => {
|
|
776
|
+
const host = ctx.ftpsHost || hostOf(ctx.origin);
|
|
777
|
+
const port = ctx.ftpsPort || 2121;
|
|
778
|
+
const user = ctx.sftpLogin || userOf(ctx, t);
|
|
779
|
+
const secret = t('conn.guide.secretPlaceholder');
|
|
780
|
+
const target = ctx.storage ? `/${ctx.storage}` : '/<storage>';
|
|
781
|
+
const pasv =
|
|
782
|
+
ctx.ftpsPasvMin && ctx.ftpsPasvMax ? `${ctx.ftpsPasvMin}-${ctx.ftpsPasvMax}` : '30000-30100';
|
|
783
|
+
|
|
784
|
+
const facts: GuideFact[] = [
|
|
785
|
+
{ label: t('conn.guide.ftps.fact.host'), value: host, hint: t('conn.guide.ftps.fact.hostHint') },
|
|
786
|
+
{ label: t('conn.guide.ftps.fact.port'), value: String(port), hint: t('conn.guide.ftps.fact.portHint') },
|
|
787
|
+
{ label: t('conn.guide.ftps.fact.mode'), value: t('conn.guide.ftps.fact.modeValue'), hint: t('conn.guide.ftps.fact.modeHint') },
|
|
788
|
+
{ label: t('conn.guide.fact.user'), value: user, hint: t('conn.guide.ftps.fact.userHint') },
|
|
789
|
+
{ label: t('conn.guide.fact.password'), value: secret, hint: t('conn.guide.ftps.fact.passwordHint'), placeholderOnly: true },
|
|
790
|
+
{ label: t('conn.guide.ftps.fact.pasv'), value: pasv, hint: t('conn.guide.ftps.fact.pasvHint') },
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
const clients: GuideClient[] = [
|
|
794
|
+
{
|
|
795
|
+
id: 'filezilla',
|
|
796
|
+
name: 'FileZilla',
|
|
797
|
+
platform: 'any',
|
|
798
|
+
blocks: [
|
|
799
|
+
{
|
|
800
|
+
kind: 'steps',
|
|
801
|
+
steps: [
|
|
802
|
+
t('conn.guide.ftps.filezilla.s1'),
|
|
803
|
+
t('conn.guide.ftps.filezilla.s2', { host, port: String(port) }),
|
|
804
|
+
t('conn.guide.ftps.filezilla.s3', { user }),
|
|
805
|
+
t('conn.guide.ftps.filezilla.s4'),
|
|
806
|
+
],
|
|
807
|
+
},
|
|
808
|
+
],
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
id: 'winscp',
|
|
812
|
+
name: 'WinSCP',
|
|
813
|
+
platform: 'windows',
|
|
814
|
+
blocks: [
|
|
815
|
+
{
|
|
816
|
+
kind: 'steps',
|
|
817
|
+
steps: [
|
|
818
|
+
t('conn.guide.ftps.winscp.s1'),
|
|
819
|
+
t('conn.guide.ftps.winscp.s2', { host, port: String(port) }),
|
|
820
|
+
t('conn.guide.ftps.winscp.s3', { user }),
|
|
821
|
+
],
|
|
822
|
+
},
|
|
823
|
+
],
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
id: 'curl',
|
|
827
|
+
name: 'curl',
|
|
828
|
+
platform: 'any',
|
|
829
|
+
blocks: [
|
|
830
|
+
{
|
|
831
|
+
kind: 'code',
|
|
832
|
+
caption: t('conn.guide.ftps.curl.caption'),
|
|
833
|
+
code: [
|
|
834
|
+
`curl --ssl-reqd --ftp-pasv --user ${user} \\`,
|
|
835
|
+
` -T ./report.pdf "ftp://${host}:${port}${target}/report.pdf"`,
|
|
836
|
+
'',
|
|
837
|
+
`curl --ssl-reqd --ftp-pasv --user ${user} \\`,
|
|
838
|
+
` -o ./report.pdf "ftp://${host}:${port}${target}/report.pdf"`,
|
|
839
|
+
].join('\n'),
|
|
840
|
+
},
|
|
841
|
+
// ⚠ The flag that matters. Without it curl will happily fall back to
|
|
842
|
+
// plaintext against a server that allows it — this one does not, but
|
|
843
|
+
// the habit is what protects you against the ones that do.
|
|
844
|
+
{ kind: 'note', text: t('conn.guide.ftps.curl.sslReqd') },
|
|
845
|
+
],
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: 'lftp',
|
|
849
|
+
name: 'lftp',
|
|
850
|
+
platform: 'any',
|
|
851
|
+
blocks: [
|
|
852
|
+
{
|
|
853
|
+
kind: 'code',
|
|
854
|
+
caption: '~/.lftprc',
|
|
855
|
+
code: [
|
|
856
|
+
'set ftp:ssl-force true',
|
|
857
|
+
'set ftp:ssl-protect-data true',
|
|
858
|
+
'set ftp:passive-mode true',
|
|
859
|
+
].join('\n'),
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
kind: 'code',
|
|
863
|
+
caption: t('conn.guide.ftps.lftp.caption'),
|
|
864
|
+
code: [
|
|
865
|
+
`lftp -u ${user} ftp://${host}:${port}`,
|
|
866
|
+
`# then: cd ${target}; put ./report.pdf; ls`,
|
|
867
|
+
].join('\n'),
|
|
868
|
+
},
|
|
869
|
+
{ kind: 'note', text: t('conn.guide.ftps.lftp.protectData') },
|
|
870
|
+
],
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
id: 'rclone',
|
|
874
|
+
name: 'rclone',
|
|
875
|
+
platform: 'any',
|
|
876
|
+
blocks: [
|
|
877
|
+
{
|
|
878
|
+
kind: 'code',
|
|
879
|
+
caption: '~/.config/rclone/rclone.conf',
|
|
880
|
+
code: [
|
|
881
|
+
'[filex-ftp]',
|
|
882
|
+
'type = ftp',
|
|
883
|
+
`host = ${host}`,
|
|
884
|
+
`port = ${port}`,
|
|
885
|
+
`user = ${user}`,
|
|
886
|
+
'# rclone obscure "<your password>"',
|
|
887
|
+
'pass = …',
|
|
888
|
+
'explicit_tls = true',
|
|
889
|
+
].join('\n'),
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
kind: 'code',
|
|
893
|
+
caption: t('conn.guide.ftps.rclone.useCaption'),
|
|
894
|
+
code: [
|
|
895
|
+
'rclone lsd filex-ftp:',
|
|
896
|
+
`rclone copy ./local filex-ftp:${target}/backup -P`,
|
|
897
|
+
].join('\n'),
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
id: 'printer',
|
|
903
|
+
name: t('conn.guide.ftps.printer.tab'),
|
|
904
|
+
platform: 'any',
|
|
905
|
+
blocks: [
|
|
906
|
+
{
|
|
907
|
+
kind: 'steps',
|
|
908
|
+
steps: [
|
|
909
|
+
t('conn.guide.ftps.printer.s1', { host, port: String(port) }),
|
|
910
|
+
t('conn.guide.ftps.printer.s2', { user }),
|
|
911
|
+
t('conn.guide.ftps.printer.s3', { path: target }),
|
|
912
|
+
t('conn.guide.ftps.printer.s4'),
|
|
913
|
+
],
|
|
914
|
+
},
|
|
915
|
+
// ⚠ The honest warning: plenty of scan-to-FTP firmware cannot do TLS
|
|
916
|
+
// at all, and this endpoint will not talk to it. Saying so beats an
|
|
917
|
+
// afternoon spent debugging a device that was never going to connect.
|
|
918
|
+
{ kind: 'warn', text: t('conn.guide.ftps.printer.noTLS') },
|
|
919
|
+
],
|
|
920
|
+
},
|
|
921
|
+
];
|
|
922
|
+
|
|
923
|
+
const notes: GuideBlock[] = [
|
|
924
|
+
{ kind: 'warn', text: t('conn.guide.ftps.note.tls') },
|
|
925
|
+
{ kind: 'note', text: t('conn.guide.ftps.note.passive') },
|
|
926
|
+
{ kind: 'note', text: t('conn.guide.ftps.note.storages') },
|
|
927
|
+
{ kind: 'note', text: t('conn.guide.ftps.note.trash') },
|
|
928
|
+
{ kind: 'note', text: t('conn.guide.ftps.note.prefer') },
|
|
929
|
+
];
|
|
930
|
+
if (ctx.ftpsSelfSigned) {
|
|
931
|
+
notes.push({ kind: 'warn', text: t('conn.guide.ftps.note.selfSigned') });
|
|
932
|
+
}
|
|
933
|
+
if (ctx.ftpsEnabled === false) {
|
|
934
|
+
notes.unshift({ kind: 'warn', text: t('conn.guide.ftps.note.disabled') });
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
id: 'ftps',
|
|
939
|
+
name: 'FTPS',
|
|
940
|
+
summary: t('conn.guide.ftps.summary'),
|
|
941
|
+
facts,
|
|
942
|
+
clients,
|
|
943
|
+
notes,
|
|
944
|
+
};
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
949
|
+
// NFS
|
|
950
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Every command here was RUN against the endpoint on 2026-08-16 with the LINUX
|
|
954
|
+
* KERNEL's own NFSv3 client — including the two options a mount fails silently
|
|
955
|
+
* without (`port=` and `mountport=`, because filex serves both on one port and
|
|
956
|
+
* runs no portmapper).
|
|
957
|
+
*/
|
|
958
|
+
export const buildNfsGuide: GuideBuilder = (ctx, t) => {
|
|
959
|
+
const host = ctx.nfsHost || hostOf(ctx.origin);
|
|
960
|
+
const port = ctx.nfsPort || 2049;
|
|
961
|
+
const path = ctx.nfsPath || t('conn.guide.nfs.pathPlaceholder');
|
|
962
|
+
const ro = ctx.nfsReadOnly ? ',ro' : '';
|
|
963
|
+
const opts = `nfsvers=3,tcp,port=${port},mountport=${port},nolock${ro}`;
|
|
964
|
+
|
|
965
|
+
const facts: GuideFact[] = [
|
|
966
|
+
{ label: t('conn.guide.nfs.fact.host'), value: host, hint: t('conn.guide.nfs.fact.hostHint') },
|
|
967
|
+
{ label: t('conn.guide.nfs.fact.port'), value: String(port), hint: t('conn.guide.nfs.fact.portHint') },
|
|
968
|
+
{
|
|
969
|
+
label: t('conn.guide.nfs.fact.export'),
|
|
970
|
+
value: path,
|
|
971
|
+
hint: t('conn.guide.nfs.fact.exportHint'),
|
|
972
|
+
// ⚠ Only real right after minting: the path is stored hashed and cannot
|
|
973
|
+
// be shown again, and printing something path-shaped would produce a
|
|
974
|
+
// mount line that fails with no clue why.
|
|
975
|
+
placeholderOnly: !ctx.nfsPath,
|
|
976
|
+
},
|
|
977
|
+
{ label: t('conn.guide.nfs.fact.options'), value: opts, hint: t('conn.guide.nfs.fact.optionsHint') },
|
|
978
|
+
];
|
|
979
|
+
|
|
980
|
+
const clients: GuideClient[] = [
|
|
981
|
+
{
|
|
982
|
+
id: 'linux',
|
|
983
|
+
name: 'Linux',
|
|
984
|
+
platform: 'linux',
|
|
985
|
+
blocks: [
|
|
986
|
+
{
|
|
987
|
+
kind: 'code',
|
|
988
|
+
caption: t('conn.guide.nfs.linux.mountCaption'),
|
|
989
|
+
code: [
|
|
990
|
+
'sudo mkdir -p /mnt/filex',
|
|
991
|
+
`sudo mount -t nfs -o ${opts} ${host}:${path} /mnt/filex`,
|
|
992
|
+
'',
|
|
993
|
+
'# and to unmount:',
|
|
994
|
+
'sudo umount /mnt/filex',
|
|
995
|
+
].join('\n'),
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
kind: 'code',
|
|
999
|
+
caption: '/etc/fstab',
|
|
1000
|
+
code: `${host}:${path} /mnt/filex nfs ${opts},_netdev,noauto,x-systemd.automount 0 0`,
|
|
1001
|
+
},
|
|
1002
|
+
// ⚠ The warning that belongs next to the fstab line rather than in a
|
|
1003
|
+
// document: that file is world-readable on most systems.
|
|
1004
|
+
{ kind: 'warn', text: t('conn.guide.nfs.linux.fstabSecret') },
|
|
1005
|
+
],
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: 'macos',
|
|
1009
|
+
name: 'macOS',
|
|
1010
|
+
platform: 'macos',
|
|
1011
|
+
blocks: [
|
|
1012
|
+
{
|
|
1013
|
+
kind: 'code',
|
|
1014
|
+
caption: t('conn.guide.nfs.mac.mountCaption'),
|
|
1015
|
+
code: [
|
|
1016
|
+
'sudo mkdir -p /Volumes/filex',
|
|
1017
|
+
`sudo mount -t nfs -o vers=3,tcp,port=${port},mountport=${port},nolock${ro},resvport ${host}:${path} /Volumes/filex`,
|
|
1018
|
+
].join('\n'),
|
|
1019
|
+
},
|
|
1020
|
+
{ kind: 'note', text: t('conn.guide.nfs.mac.resvport') },
|
|
1021
|
+
],
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
id: 'windows',
|
|
1025
|
+
name: 'Windows (Client for NFS)',
|
|
1026
|
+
platform: 'windows',
|
|
1027
|
+
blocks: [
|
|
1028
|
+
{
|
|
1029
|
+
kind: 'steps',
|
|
1030
|
+
steps: [
|
|
1031
|
+
t('conn.guide.nfs.win.s1'),
|
|
1032
|
+
t('conn.guide.nfs.win.s2'),
|
|
1033
|
+
t('conn.guide.nfs.win.s3'),
|
|
1034
|
+
],
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
kind: 'code',
|
|
1038
|
+
caption: t('conn.guide.nfs.win.cmdCaption'),
|
|
1039
|
+
code: `mount -o anon nolock ${host}:${path} Z:`,
|
|
1040
|
+
},
|
|
1041
|
+
// ⚠ Windows' client has no way to say "the mount service is on this
|
|
1042
|
+
// port", so it only works when filex is on 2049.
|
|
1043
|
+
{ kind: 'warn', text: t('conn.guide.nfs.win.port') },
|
|
1044
|
+
],
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
id: 'synology',
|
|
1048
|
+
name: t('conn.guide.nfs.nas.tab'),
|
|
1049
|
+
platform: 'any',
|
|
1050
|
+
blocks: [
|
|
1051
|
+
{
|
|
1052
|
+
kind: 'steps',
|
|
1053
|
+
steps: [
|
|
1054
|
+
t('conn.guide.nfs.nas.s1', { host, port: String(port) }),
|
|
1055
|
+
t('conn.guide.nfs.nas.s2'),
|
|
1056
|
+
t('conn.guide.nfs.nas.s3'),
|
|
1057
|
+
],
|
|
1058
|
+
},
|
|
1059
|
+
],
|
|
1060
|
+
},
|
|
1061
|
+
];
|
|
1062
|
+
|
|
1063
|
+
const notes: GuideBlock[] = [
|
|
1064
|
+
{ kind: 'warn', text: t('conn.guide.nfs.note.unencrypted') },
|
|
1065
|
+
{ kind: 'warn', text: t('conn.guide.nfs.note.pathIsSecret') },
|
|
1066
|
+
{ kind: 'note', text: t('conn.guide.nfs.note.noPortmapper') },
|
|
1067
|
+
{ kind: 'note', text: t('conn.guide.nfs.note.uid') },
|
|
1068
|
+
{ kind: 'note', text: t('conn.guide.nfs.note.trash') },
|
|
1069
|
+
{ kind: 'note', text: t('conn.guide.nfs.note.revoke') },
|
|
1070
|
+
];
|
|
1071
|
+
if (ctx.nfsEnabled === false) {
|
|
1072
|
+
notes.unshift({ kind: 'warn', text: t('conn.guide.nfs.note.disabled') });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
id: 'nfs',
|
|
1077
|
+
name: 'NFS',
|
|
1078
|
+
summary: t('conn.guide.nfs.summary'),
|
|
1079
|
+
facts,
|
|
1080
|
+
clients,
|
|
1081
|
+
notes,
|
|
1082
|
+
};
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1087
|
+
// filex mount (FUSE)
|
|
1088
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* `filex mount` is the odd one out here: there is no server to point a
|
|
1092
|
+
* third-party client at, because filex's OWN binary is the client. It speaks
|
|
1093
|
+
* the REST API over the same HTTPS the browser uses, which is what makes it
|
|
1094
|
+
* the only one of these that works from anywhere — NFS needs a LAN, SFTP needs
|
|
1095
|
+
* sshfs or WinFsp already configured, and this needs a URL and a token.
|
|
1096
|
+
*
|
|
1097
|
+
* ⚠ It is not a sync. Nothing is copied except a bounded read cache, so it
|
|
1098
|
+
* opens one file out of a hundred thousand without downloading the rest;
|
|
1099
|
+
* `filex sync` is still the answer for having the files offline. The guide says
|
|
1100
|
+
* so, because somebody who picks the wrong one finds out slowly.
|
|
1101
|
+
*/
|
|
1102
|
+
export const buildMountGuide: GuideBuilder = (ctx, t) => {
|
|
1103
|
+
const origin = ctx.origin.replace(/\/+$/, '');
|
|
1104
|
+
const storage = ctx.storage || ctx.storages[0] || 'main';
|
|
1105
|
+
|
|
1106
|
+
const facts: GuideFact[] = [
|
|
1107
|
+
{ label: t('conn.guide.mount.fact.url'), value: origin, hint: t('conn.guide.mount.fact.urlHint') },
|
|
1108
|
+
{
|
|
1109
|
+
label: t('conn.guide.mount.fact.token'),
|
|
1110
|
+
value: t('conn.guide.mount.fact.tokenPlaceholder'),
|
|
1111
|
+
hint: t('conn.guide.mount.fact.tokenHint'),
|
|
1112
|
+
// filex never has the plaintext of a token after it is minted, and a
|
|
1113
|
+
// fact row that looked like one would be a lie the user pastes.
|
|
1114
|
+
placeholderOnly: true,
|
|
1115
|
+
},
|
|
1116
|
+
{ label: t('conn.guide.mount.fact.remote'), value: `${storage}://`, hint: t('conn.guide.mount.fact.remoteHint') },
|
|
1117
|
+
];
|
|
1118
|
+
|
|
1119
|
+
const clients: GuideClient[] = [
|
|
1120
|
+
{
|
|
1121
|
+
id: 'linux',
|
|
1122
|
+
name: 'Linux',
|
|
1123
|
+
platform: 'linux',
|
|
1124
|
+
blocks: [
|
|
1125
|
+
{
|
|
1126
|
+
kind: 'code',
|
|
1127
|
+
caption: t('conn.guide.mount.linux.mountCaption'),
|
|
1128
|
+
code: [
|
|
1129
|
+
`export FILEX_URL=${origin}`,
|
|
1130
|
+
'export FILEX_TOKEN=<token>',
|
|
1131
|
+
'',
|
|
1132
|
+
'mkdir -p ~/filex',
|
|
1133
|
+
'filex mount ~/filex',
|
|
1134
|
+
'',
|
|
1135
|
+
`# one storage only, or a subfolder of it:`,
|
|
1136
|
+
`filex mount --remote '${storage}://' ~/filex`,
|
|
1137
|
+
`filex mount --remote '${storage}://projects/acme' --read-only ~/acme`,
|
|
1138
|
+
].join('\n'),
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
kind: 'code',
|
|
1142
|
+
caption: t('conn.guide.mount.linux.umountCaption'),
|
|
1143
|
+
code: 'fusermount -u ~/filex',
|
|
1144
|
+
},
|
|
1145
|
+
// ⚠ The one that costs an afternoon if it is not said: killing the
|
|
1146
|
+
// process leaves a directory where every `ls` hangs.
|
|
1147
|
+
{ kind: 'warn', text: t('conn.guide.mount.linux.umountWarn') },
|
|
1148
|
+
{
|
|
1149
|
+
kind: 'code',
|
|
1150
|
+
caption: t('conn.guide.mount.linux.systemdCaption'),
|
|
1151
|
+
code: [
|
|
1152
|
+
'# ~/.config/systemd/user/filex-mount.service',
|
|
1153
|
+
'[Unit]',
|
|
1154
|
+
'Description=filex mount',
|
|
1155
|
+
'After=network-online.target',
|
|
1156
|
+
'',
|
|
1157
|
+
'[Service]',
|
|
1158
|
+
`Environment=FILEX_URL=${origin}`,
|
|
1159
|
+
'Environment=FILEX_TOKEN=<token>',
|
|
1160
|
+
'ExecStart=%h/.local/bin/filex mount %h/filex',
|
|
1161
|
+
'ExecStop=/bin/fusermount -u %h/filex',
|
|
1162
|
+
'Restart=on-failure',
|
|
1163
|
+
'',
|
|
1164
|
+
'[Install]',
|
|
1165
|
+
'WantedBy=default.target',
|
|
1166
|
+
].join('\n'),
|
|
1167
|
+
},
|
|
1168
|
+
],
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
id: 'windows',
|
|
1172
|
+
name: 'Windows',
|
|
1173
|
+
platform: 'windows',
|
|
1174
|
+
blocks: [
|
|
1175
|
+
{ kind: 'note', text: t('conn.guide.mount.win.winfsp') },
|
|
1176
|
+
{
|
|
1177
|
+
kind: 'code',
|
|
1178
|
+
caption: t('conn.guide.mount.win.mountCaption'),
|
|
1179
|
+
code: [
|
|
1180
|
+
`$env:FILEX_URL = "${origin}"`,
|
|
1181
|
+
'$env:FILEX_TOKEN = "<token>"',
|
|
1182
|
+
'',
|
|
1183
|
+
'filex mount Z:',
|
|
1184
|
+
].join('\n'),
|
|
1185
|
+
},
|
|
1186
|
+
// ⚠ The drive letter is CREATED, not reused — pointing it at one that
|
|
1187
|
+
// already exists is the commonest way this fails, and the message the
|
|
1188
|
+
// driver gives for it says nothing useful.
|
|
1189
|
+
{ kind: 'warn', text: t('conn.guide.mount.win.freeLetter') },
|
|
1190
|
+
{ kind: 'note', text: t('conn.guide.mount.win.stop') },
|
|
1191
|
+
],
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
id: 'macos',
|
|
1195
|
+
name: 'macOS',
|
|
1196
|
+
platform: 'macos',
|
|
1197
|
+
blocks: [
|
|
1198
|
+
// ⚠⚠ A refusal stated up front rather than discovered. macFUSE's Go
|
|
1199
|
+
// binding needs a C toolchain filex deliberately does not use, and its
|
|
1200
|
+
// licence forbids a commercial program from installing it. Pretending
|
|
1201
|
+
// otherwise would mean a command that appears to work and does nothing.
|
|
1202
|
+
{ kind: 'warn', text: t('conn.guide.mount.mac.unsupported') },
|
|
1203
|
+
{ kind: 'note', text: t('conn.guide.mount.mac.alternatives') },
|
|
1204
|
+
],
|
|
1205
|
+
},
|
|
1206
|
+
];
|
|
1207
|
+
|
|
1208
|
+
const notes: GuideBlock[] = [
|
|
1209
|
+
{ kind: 'note', text: t('conn.guide.mount.note.notASync') },
|
|
1210
|
+
{ kind: 'note', text: t('conn.guide.mount.note.reachable') },
|
|
1211
|
+
{ kind: 'note', text: t('conn.guide.mount.note.wholeFileWrites') },
|
|
1212
|
+
{ kind: 'note', text: t('conn.guide.mount.note.trash') },
|
|
1213
|
+
{ kind: 'note', text: t('conn.guide.mount.note.revoke') },
|
|
1214
|
+
];
|
|
1215
|
+
|
|
1216
|
+
return {
|
|
1217
|
+
id: 'mount',
|
|
1218
|
+
name: 'filex mount',
|
|
1219
|
+
summary: t('conn.guide.mount.summary'),
|
|
1220
|
+
facts,
|
|
1221
|
+
clients,
|
|
1222
|
+
notes,
|
|
1223
|
+
};
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1227
|
+
// Registry
|
|
1228
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Protocol id → builder.
|
|
1232
|
+
*
|
|
1233
|
+
* S3 and SFTP land here as one entry each once their servers exist; the
|
|
1234
|
+
* panel, the copy buttons, the client tabs and the i18n plumbing are
|
|
1235
|
+
* already written for them. That is the whole point of the shape.
|
|
1236
|
+
*/
|
|
1237
|
+
export const GUIDE_BUILDERS: Record<string, GuideBuilder> = {
|
|
1238
|
+
webdav: buildWebdavGuide,
|
|
1239
|
+
s3: buildS3Guide,
|
|
1240
|
+
sftp: buildSftpGuide,
|
|
1241
|
+
ftps: buildFtpsGuide,
|
|
1242
|
+
nfs: buildNfsGuide,
|
|
1243
|
+
mount: buildMountGuide,
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Display name per protocol id.
|
|
1248
|
+
*
|
|
1249
|
+
* ⚠ Not `id.toUpperCase()`, which the picker used to do. That reads fine for
|
|
1250
|
+
* S3 and NFS and turns `filex mount` into "MOUNT" — a name for a thing that is
|
|
1251
|
+
* not a protocol at all, in a list where every other entry is one.
|
|
1252
|
+
*/
|
|
1253
|
+
export const GUIDE_NAMES: Record<string, string> = {
|
|
1254
|
+
webdav: 'WebDAV',
|
|
1255
|
+
s3: 'S3',
|
|
1256
|
+
sftp: 'SFTP',
|
|
1257
|
+
ftps: 'FTPS',
|
|
1258
|
+
nfs: 'NFS',
|
|
1259
|
+
mount: 'filex mount',
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
/** The label to show for a protocol id in a picker. */
|
|
1263
|
+
export function guideName(id: string): string {
|
|
1264
|
+
return GUIDE_NAMES[id] || id.toUpperCase();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** Protocol ids that have a guide, in display order. */
|
|
1268
|
+
export function guideProtocols(): string[] {
|
|
1269
|
+
return Object.keys(GUIDE_BUILDERS);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export function buildGuide(
|
|
1273
|
+
protocol: string,
|
|
1274
|
+
ctx: GuideContext,
|
|
1275
|
+
t: Translate,
|
|
1276
|
+
): ProtocolGuide | null {
|
|
1277
|
+
const builder = GUIDE_BUILDERS[protocol];
|
|
1278
|
+
return builder ? builder(ctx, t) : null;
|
|
1279
|
+
}
|