@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.
Files changed (39) hide show
  1. package/README.md +34 -4
  2. package/dist/filex-core.js +10626 -6501
  3. package/dist/filex-core.js.map +1 -1
  4. package/dist/filex-core.umd.cjs +94 -63
  5. package/dist/filex-core.umd.cjs.map +1 -1
  6. package/dist/index.d.ts +1026 -7
  7. package/dist/style.css +1 -1
  8. package/package.json +3 -3
  9. package/src/FileExplorer.vue +103 -68
  10. package/src/components/ConnectionGuideView.vue +333 -0
  11. package/src/components/ConnectionsPanel.vue +912 -0
  12. package/src/components/NFSExportsPanel.vue +283 -0
  13. package/src/components/S3KeysPanel.vue +381 -0
  14. package/src/components/SSHKeysPanel.vue +222 -0
  15. package/src/components/StorageFields.vue +362 -0
  16. package/src/components/TokensPanel.vue +191 -0
  17. package/src/components/UploadProgress.vue +5 -1
  18. package/src/composables/useConnections.ts +271 -0
  19. package/src/composables/useFileApi.ts +15 -2
  20. package/src/composables/useNFSExports.ts +148 -0
  21. package/src/composables/useS3Keys.ts +175 -0
  22. package/src/composables/useSSHKeys.ts +119 -0
  23. package/src/composables/useThumbs.ts +1 -1
  24. package/src/composables/useTokens.ts +121 -0
  25. package/src/composables/useUploadChunked.ts +433 -164
  26. package/src/index.ts +77 -2
  27. package/src/lib/connectionGuides.ts +1279 -0
  28. package/src/lib/realtime.ts +1 -1
  29. package/src/lib/uploadResume.ts +157 -0
  30. package/src/locales/en.ts +413 -0
  31. package/src/locales/tr.ts +416 -0
  32. package/src/modals/ConvertModal.vue +1 -1
  33. package/src/styles/base.css +12 -12
  34. package/src/types/Connections.ts +122 -0
  35. package/src/types/ExplorerConfig.ts +23 -2
  36. package/src/types/NFSExports.ts +47 -0
  37. package/src/types/S3Keys.ts +55 -0
  38. package/src/types/SSHKeys.ts +54 -0
  39. package/src/types/Tokens.ts +39 -0
@@ -3,7 +3,7 @@
3
3
  // webcomponent embedded in host apps — gets live folder updates + presence.
4
4
  //
5
5
  // Auth: the browser's native WebSocket can't set an Authorization header and,
6
- // when embedded, connects cross-origin to fm.brf.sh. So instead of a header we
6
+ // when embedded, connects cross-origin to fm.example.com. So instead of a header we
7
7
  // fetch a short-lived, single-use TICKET through the host's normal API (which
8
8
  // injects the real token server-side) and open `wss://…/api/ws?ticket=<t>`.
9
9
  // The durable token never reaches the browser.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * uploadResume — where a browser upload writes down that it is not finished.
3
+ *
4
+ * A staged upload survives a dropped connection because the SERVER holds the
5
+ * bytes and can be asked for its offset. What the browser has to survive is
6
+ * itself: a reloaded tab, a closed laptop, a crashed renderer. None of those
7
+ * reach the server, and without a note on disk the next visit has no idea an
8
+ * upload id ever existed — so the same 4 GB starts again at zero, which is the
9
+ * exact complaint this work exists to answer.
10
+ *
11
+ * The note is deliberately small and deliberately a hint:
12
+ *
13
+ * - it stores the upload id, not the bytes. localStorage cannot hold a File,
14
+ * and the browser will not hand a File back without a fresh user gesture.
15
+ * Recovery is therefore "pick the same file again and it continues", not
16
+ * "it silently continues" — the honest shape, and the one the platform
17
+ * allows.
18
+ * - `offset` here is never used as a resume point. It is shown to the user
19
+ * ("resuming at 62%") and used to decide a session is worth asking about;
20
+ * the byte to continue from always comes from GET /api/files/upload/{id}.
21
+ *
22
+ * The fingerprint pins a record to one file: name, size and lastModified. A
23
+ * different file with the same name must not inherit the session, because
24
+ * appending its tail to the previous head is the one way a resumable upload
25
+ * corrupts data.
26
+ *
27
+ * This lives in `packages/core` because it is not browser-chrome: the web
28
+ * explorer, the desktop app's explorer and the work.example.com / fishapp embeds all
29
+ * mount the same component, and an upload that resumes in one of them and not
30
+ * the others would be two products again.
31
+ */
32
+
33
+ const STORE_KEY = 'filex:uploads:v1';
34
+
35
+ /** How long an unfinished record is worth keeping. The server sweeps its own
36
+ * staging after FILEX_UPLOAD_STAGING_TTL (24 h by default); a note that
37
+ * outlives the bytes it describes only produces a confusing "resuming…" that
38
+ * immediately restarts. */
39
+ export const RESUME_TTL_MS = 24 * 60 * 60 * 1000;
40
+
41
+ export interface ResumeRecord {
42
+ /** Server-side staged upload id. */
43
+ uploadId: string;
44
+ /** Destination directory, qualified (`adapter://sub/dir`). */
45
+ path: string;
46
+ name: string;
47
+ size: number;
48
+ /** File.lastModified — part of the identity, not metadata. */
49
+ lastModified: number;
50
+ chunkSize: number;
51
+ /** Last offset the server acknowledged. Display + triage only. */
52
+ offset: number;
53
+ updatedAt: number;
54
+ }
55
+
56
+ /** Identity of one (destination, file) pair. */
57
+ export function uploadFingerprint(
58
+ path: string,
59
+ file: { name: string; size: number; lastModified?: number },
60
+ ): string {
61
+ return [path, file.name, file.size, file.lastModified ?? 0].join('\u0000');
62
+ }
63
+
64
+ type Store = Record<string, ResumeRecord>;
65
+
66
+ /** A Storage-shaped thing. Injected so tests (and any surface without
67
+ * localStorage — SSR, a locked-down embed) do not have to fake globals. */
68
+ export interface ResumeStorage {
69
+ getItem(key: string): string | null;
70
+ setItem(key: string, value: string): void;
71
+ removeItem(key: string): void;
72
+ }
73
+
74
+ /** The default backing store, or null where the platform has none. Reading
75
+ * localStorage throws in a partitioned/blocked context, so the probe is a real
76
+ * try, not a typeof check. */
77
+ export function defaultResumeStorage(): ResumeStorage | null {
78
+ try {
79
+ if (typeof localStorage === 'undefined') return null;
80
+ localStorage.getItem(STORE_KEY);
81
+ return localStorage;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function read(store: ResumeStorage | null): Store {
88
+ if (!store) return {};
89
+ try {
90
+ const raw = store.getItem(STORE_KEY);
91
+ if (!raw) return {};
92
+ const parsed = JSON.parse(raw) as Store;
93
+ return parsed && typeof parsed === 'object' ? parsed : {};
94
+ } catch {
95
+ // Corrupt payload: an upload that starts over is a far smaller problem
96
+ // than one that throws on every page load.
97
+ return {};
98
+ }
99
+ }
100
+
101
+ function write(store: ResumeStorage | null, data: Store): void {
102
+ if (!store) return;
103
+ try {
104
+ store.setItem(STORE_KEY, JSON.stringify(data));
105
+ } catch {
106
+ /* quota / private mode — resume degrades, uploads still work */
107
+ }
108
+ }
109
+
110
+ /** Drop records older than RESUME_TTL_MS. Returns what survived. */
111
+ export function pruneResume(store: ResumeStorage | null, now = Date.now()): Store {
112
+ const data = read(store);
113
+ let changed = false;
114
+ for (const [key, rec] of Object.entries(data)) {
115
+ if (!rec?.uploadId || now - (rec.updatedAt ?? 0) > RESUME_TTL_MS) {
116
+ delete data[key];
117
+ changed = true;
118
+ }
119
+ }
120
+ if (changed) write(store, data);
121
+ return data;
122
+ }
123
+
124
+ /** The record for this exact (destination, file), or null. */
125
+ export function loadResume(
126
+ store: ResumeStorage | null,
127
+ key: string,
128
+ now = Date.now(),
129
+ ): ResumeRecord | null {
130
+ const rec = pruneResume(store, now)[key];
131
+ return rec ?? null;
132
+ }
133
+
134
+ export function saveResume(
135
+ store: ResumeStorage | null,
136
+ key: string,
137
+ rec: Omit<ResumeRecord, 'updatedAt'>,
138
+ now = Date.now(),
139
+ ): void {
140
+ const data = read(store);
141
+ data[key] = { ...rec, updatedAt: now };
142
+ write(store, data);
143
+ }
144
+
145
+ export function clearResume(store: ResumeStorage | null, key: string): void {
146
+ const data = read(store);
147
+ if (key in data) {
148
+ delete data[key];
149
+ write(store, data);
150
+ }
151
+ }
152
+
153
+ /** Every unfinished upload this browser knows about, newest first. Surfaces
154
+ * use it to say "you have an unfinished upload" instead of forgetting. */
155
+ export function listResume(store: ResumeStorage | null, now = Date.now()): ResumeRecord[] {
156
+ return Object.values(pruneResume(store, now)).sort((a, b) => b.updatedAt - a.updatedAt);
157
+ }
package/src/locales/en.ts CHANGED
@@ -59,6 +59,9 @@ export const en: Record<string, string> = {
59
59
  'upload.failed': 'Could not upload “{name}”',
60
60
  'upload.aborted': 'Aborted',
61
61
  'upload.cancel': 'Cancel',
62
+ 'upload.resuming': 'Resuming “{name}” from {percent}%',
63
+ 'upload.committing': 'Finishing',
64
+ 'upload.transferring': 'Moving to storage',
62
65
 
63
66
  'empty.folder': 'This folder is empty',
64
67
  'empty.search': 'No results',
@@ -407,4 +410,414 @@ export const en: Record<string, string> = {
407
410
  'e2e.decrypt_failed': 'Could not decrypt the file (password changed or file corrupted).',
408
411
  'e2e.download.failed': 'Could not download the encrypted file.',
409
412
  /* /wiring:e2 */
413
+
414
+ /* ── connections: storage config + "how to connect" ──────────────
415
+ `storages.*` keys are the ones the BACKEND's driver descriptors
416
+ name (each driver package declares one next to its Init). They are
417
+ kept identical to web/src/locales/*.json on purpose: the same
418
+ descriptor is rendered on both surfaces, so a field that reads
419
+ "Base path" in the admin panel and something else in the desktop
420
+ app would be the same drift this whole mechanism exists to stop. */
421
+ 'conn.title': 'Storage connections',
422
+ 'conn.subtitle': 'Connect {host} to a backend, or connect your computer to {host}.',
423
+ 'conn.close': 'Close',
424
+ 'conn.tab.storages': 'Storages',
425
+ 'conn.tab.connect': 'How to connect',
426
+ 'conn.loading': 'Loading…',
427
+ 'conn.visibleOnly': 'you can browse this one',
428
+ 'conn.denied.title': 'Only an administrator can add storages here',
429
+ 'conn.denied.none': 'Your account may browse files but not change what this server is connected to. Ask an administrator of this server to add or edit a storage.',
430
+ 'conn.denied.anonymous': 'You are not signed in on this server.',
431
+ 'conn.denied.unreachable': 'The server did not answer: {error}',
432
+ 'conn.denied.guideHint': 'You do not need administrator rights to connect your own computer to the storages you can already see.',
433
+ 'conn.denied.guideCta': 'Show me how to connect',
434
+ 'conn.list.count': '{n} storage(s)',
435
+ 'conn.list.add': 'Add a storage',
436
+ 'conn.list.empty': 'No storage is configured yet. Add one and the files show up in the explorer.',
437
+ 'conn.list.edit': 'Edit',
438
+ 'conn.list.remove': 'Remove',
439
+ 'conn.list.confirm': 'Really remove?',
440
+ 'conn.list.readOnly': 'read-only',
441
+ 'conn.list.disabled': 'disabled',
442
+ 'conn.form.newTitle': 'New storage',
443
+ 'conn.form.editTitle': 'Edit “{name}”',
444
+ 'conn.form.name': 'Display name',
445
+ 'conn.form.namePlaceholder': 'photos',
446
+ 'conn.form.nameHelp': 'What this storage is called everywhere: in the explorer, in paths (name://folder) and in WebDAV URLs.',
447
+ 'conn.form.driver': 'Type',
448
+ 'conn.form.driverLocked': 'A storage\'s type cannot change after it is created — the paths already point at it.',
449
+ 'conn.form.readOnly': 'Read-only (nothing may be written to this storage)',
450
+ 'conn.form.enabled': 'Enabled',
451
+ 'conn.form.test': 'Test connection',
452
+ 'conn.form.testing': 'Testing…',
453
+ 'conn.form.testOk': 'Connected. {count} item(s) at the root.',
454
+ 'conn.form.testFail': 'Could not connect: {error}',
455
+ 'conn.form.save': 'Save',
456
+ 'conn.form.saving': 'Saving…',
457
+ 'conn.form.cancel': 'Cancel',
458
+ 'conn.form.nameRequired': 'Give the storage a name.',
459
+ 'conn.form.fillRequired': 'Fill in the fields marked with *.',
460
+ 'conn.form.required': 'Required',
461
+ 'conn.form.advanced': 'Advanced settings',
462
+ 'conn.form.noFields': 'This server did not describe any settings for this driver.',
463
+ 'conn.form.reveal': 'Show',
464
+ 'conn.form.hide': 'Hide',
465
+ 'conn.guide.protocol': 'Protocol',
466
+ 'conn.guide.storage': 'Storage',
467
+ 'conn.guide.allStorages': 'All storages',
468
+ 'conn.guide.copy': 'Copy',
469
+ 'conn.guide.copied': 'Copied',
470
+ 'conn.guide.goodToKnow': 'Good to know',
471
+ 'conn.guide.userPlaceholder': 'your-email@example.com',
472
+ 'conn.guide.secretPlaceholder': 'your-password-or-token',
473
+ 'conn.guide.fact.url': 'Address',
474
+ 'conn.guide.fact.urlRootHint': 'Every storage you can see appears as a folder under this address.',
475
+ 'conn.guide.fact.urlStorageHint': 'Mounts “{storage}” directly. Drop the last segment to see every storage.',
476
+ 'conn.guide.fact.user': 'Username',
477
+ 'conn.guide.fact.password': 'Password',
478
+ 'conn.guide.webdav.summary': 'WebDAV mounts filex as a drive: Windows Explorer, macOS Finder, Linux, rclone and Cyberduck all speak it, and every change is mirrored back into filex.',
479
+ 'conn.guide.webdav.userHint': 'Your filex account e-mail — not a display name.',
480
+ 'conn.guide.webdav.passwordHint': 'Your account password, or an API token (Settings → API tokens). An account with two-factor authentication MUST use a token: Basic auth has no second-factor slot.',
481
+ 'conn.guide.webdav.win.s1': 'Open File Explorer, right-click This PC and choose “Map network drive…”.',
482
+ 'conn.guide.webdav.win.s2': 'Pick a drive letter and enter {url} as the folder.',
483
+ 'conn.guide.webdav.win.s3': 'Tick “Connect using different credentials” and sign in with the username and password above.',
484
+ 'conn.guide.webdav.win.cmdCaption': 'Command Prompt (same thing, without the wizard)',
485
+ 'conn.guide.webdav.win.limits': 'Windows has three built-in limits that look exactly like filex bugs: transfers stop at ~47.7 MB, folders with about a thousand files refuse to open (“Disk is not formatted”), and the WebClient service must be restarted after changing either.',
486
+ 'conn.guide.webdav.win.regCaption': 'Run as administrator, then reconnect the drive',
487
+ 'conn.guide.webdav.win.https': 'HTTPS is mandatory. Windows sends Basic credentials over TLS only, and over plain http:// it silently refuses with no useful message. Do not set BasicAuthLevel to 2 — use TLS.',
488
+ 'conn.guide.webdav.win.persist': 'The mapped drive does not survive a sign-out: since Windows 7 Basic credentials cannot be stored in Credential Manager, and /persistent:yes does not change that. Re-run the command from a logon script if you need it back automatically.',
489
+ 'conn.guide.webdav.win.service': 'If nothing mounts at all, make sure the WebClient service is running: sc config WebClient start= auto && net start WebClient.',
490
+ 'conn.guide.webdav.mac.s1': 'In Finder choose Go → Connect to Server… (⌘K).',
491
+ 'conn.guide.webdav.mac.s2': 'Enter {url} and connect.',
492
+ 'conn.guide.webdav.mac.s3': 'Authenticate with the username and password above.',
493
+ 'conn.guide.webdav.mac.note': 'The drive appears under Locations; each storage is a top-level folder.',
494
+ 'conn.guide.webdav.linux.mountCaption': 'davfs2, or the desktop file manager (GNOME Files, Dolphin)',
495
+ 'conn.guide.webdav.linux.gvfsComment': 'or paste this address into GNOME Files / Dolphin',
496
+ 'conn.guide.webdav.linux.locks': 'davfs2 needs locking turned off. filex keeps WebDAV locks in process memory, so they do not survive a restart and are not shared between replicas — davfs2 assumes otherwise and hangs.',
497
+ 'conn.guide.webdav.rclone.obscureCaption': 'rclone stores an obscured password — run this first and paste the output below',
498
+ 'conn.guide.webdav.rclone.passPlaceholder': '<output of rclone obscure>',
499
+ 'conn.guide.webdav.rclone.useCaption': 'Using it',
500
+ 'conn.guide.webdav.duck.s1': 'New Bookmark → WebDAV (HTTPS).',
501
+ 'conn.guide.webdav.duck.s2': 'Server {host}, port 443.',
502
+ 'conn.guide.webdav.duck.s3': 'Path {path}.',
503
+ 'conn.guide.webdav.duck.s4': 'Username {user}, password as above.',
504
+ 'conn.guide.webdav.note.delete': 'A WebDAV delete is permanent. The web UI moves files to the filex trash; a client delete removes the object from the backing storage directly.',
505
+ 'conn.guide.webdav.note.locks': 'Locks live in server memory: they do not survive a restart and are not shared between replicas. They exist to satisfy Windows and Office, not to arbitrate concurrent edits.',
506
+ 'conn.guide.webdav.note.permissions': 'You see exactly what you see in the web UI. The root lists only the storages you may open, and a path outside your grants answers 404 rather than 403, so the tree never leaks what exists.',
507
+ 'conn.guide.webdav.note.http': 'This server is reachable over plain http://. Windows will refuse to send your credentials at all, and every other client sends your password in the clear. Put filex behind TLS before mounting it.',
508
+ // ── S3 endpoint: keys and the guide ──────────────────────────────
509
+ 'conn.s3keys.title': 'S3 access keys',
510
+ 'conn.s3keys.lead': 'An access key lets any S3 client — rclone, restic, the AWS CLI, mc, s3fs, Cyberduck — connect to filex. It carries your own permissions and nothing more.',
511
+ 'conn.s3keys.disabled': 'The S3 endpoint is switched off on this server (FILEX_S3). A key minted here will not connect until an operator turns it back on.',
512
+ 'conn.s3keys.cannotMint': 'You are not signed in with an account that can mint access keys. The instructions below still apply — ask for a key and paste it in.',
513
+ 'conn.s3keys.label': 'What is it for? (laptop backup, CI, …)',
514
+ 'conn.s3keys.defaultLabel': 'access key',
515
+ 'conn.s3keys.everyBucket': 'Every bucket I can see',
516
+ 'conn.s3keys.prefix': 'Folder inside the bucket (optional)',
517
+ 'conn.s3keys.mint': 'Create key',
518
+ 'conn.s3keys.inheritNote': 'A key can only ever narrow what you already have: your grants, your tenant, your role. Confining it to one bucket or folder is the difference between a laptop backup and a credential that can read everything.',
519
+ 'conn.s3keys.once': 'Copy the secret now — this is the only time it is shown.',
520
+ 'conn.s3keys.accessKeyID': 'Access key ID',
521
+ 'conn.s3keys.secret': 'Secret key',
522
+ 'conn.s3keys.dismiss': 'I have copied it',
523
+ 'conn.s3keys.col.label': 'Label',
524
+ 'conn.s3keys.col.key': 'Access key ID',
525
+ 'conn.s3keys.col.scope': 'Limited to',
526
+ 'conn.s3keys.col.lastUsed': 'Last used',
527
+ 'conn.s3keys.scopeAll': 'everything you can see',
528
+ 'conn.s3keys.noLabel': '(no label)',
529
+ 'conn.s3keys.neverUsed': 'never',
530
+ 'conn.s3keys.enable': 'Enable',
531
+ 'conn.s3keys.disable': 'Disable',
532
+ 'conn.s3keys.revoke': 'Revoke',
533
+ 'conn.s3keys.confirm': 'Sure?',
534
+ 'conn.s3keys.empty': 'No access keys yet.',
535
+ 'conn.guide.s3.summary': 'The S3 endpoint makes filex the destination for anything that speaks S3 — backups, sync tools, SDKs — using the storages you already have.',
536
+ 'conn.guide.s3.keyPlaceholder': 'create a key above',
537
+ 'conn.guide.s3.secretPlaceholder': 'shown once, when you create the key',
538
+ 'conn.guide.s3.fact.endpoint': 'Endpoint',
539
+ 'conn.guide.s3.fact.endpointHint': 'Point the client here, not at the web address of filex.',
540
+ 'conn.guide.s3.fact.bucket': 'Bucket',
541
+ 'conn.guide.s3.fact.bucketHint': 'A bucket is a filex storage. Buckets are created by an administrator, not by the client.',
542
+ 'conn.guide.s3.fact.key': 'Access key ID',
543
+ 'conn.guide.s3.fact.keyHint': 'The public half. It travels in every request.',
544
+ 'conn.guide.s3.fact.secret': 'Secret key',
545
+ 'conn.guide.s3.fact.secretHint': 'Shown once, when the key is created. filex cannot show it again — create a new key if it is lost.',
546
+ 'conn.guide.s3.fact.region': 'Region',
547
+ 'conn.guide.s3.fact.regionHint': 'filex has no regions; any value works and is echoed back. Clients still insist on one.',
548
+ 'conn.guide.s3.fact.addressing': 'Addressing',
549
+ 'conn.guide.s3.fact.pathStyle': 'path-style (required)',
550
+ 'conn.guide.s3.fact.pathStyleHint': 'There is no dedicated S3 host on this install, so bucket.host does not resolve.',
551
+ 'conn.guide.s3.fact.virtualHosted': 'virtual-hosted or path-style',
552
+ 'conn.guide.s3.fact.virtualHostedHint': 'This install has its own S3 host, so both styles work.',
553
+ 'conn.guide.s3.rclone.useCaption': 'Use it',
554
+ 'conn.guide.s3.rclone.mtime': 'filex carries the file modification time (x-amz-meta-mtime), so rclone sync settles after the first run instead of re-uploading everything.',
555
+ 'conn.guide.s3.aws.configureCaption': 'Configure once',
556
+ 'conn.guide.s3.aws.useCaption': 'Use it',
557
+ 'conn.guide.s3.aws.pathStyle': 'Add path addressing to your AWS config, or every command fails at DNS.',
558
+ 'conn.guide.s3.restic.envCaption': 'Environment',
559
+ 'conn.guide.s3.restic.useCaption': 'Back up, verify, prune',
560
+ 'conn.guide.s3.restic.verified': 'Verified against this endpoint: restic check --read-data reads every pack byte for byte and reports no errors.',
561
+ 'conn.guide.s3.mc.aliasCaption': 'Add the alias',
562
+ 'conn.guide.s3.mc.useCaption': 'Use it',
563
+ 'conn.guide.s3.s3fs.credsCaption': 'Credentials file',
564
+ 'conn.guide.s3.s3fs.mountCaption': 'Mount it',
565
+ 'conn.guide.s3.s3fs.note': 'Editing a file over s3fs rewrites the whole object, and a delete goes to the filex trash. Good for a shared folder, not for a database.',
566
+ 'conn.guide.s3.duck.s1': 'New bookmark → Amazon S3.',
567
+ 'conn.guide.s3.duck.s2': 'Server: {host}.',
568
+ 'conn.guide.s3.duck.s3': 'Access key ID: {key}, secret as above.',
569
+ 'conn.guide.s3.duck.s4': 'Open the bookmark; the buckets are your filex storages.',
570
+ 'conn.guide.s3.duck.pathStyle': 'In Preferences → S3, turn OFF "Use virtual host style" — this install has no wildcard S3 host.',
571
+ 'conn.guide.s3.sdk.note': 'Any SigV4 client works the same way: endpoint, key, secret, and path addressing when this install has no S3 host of its own.',
572
+ 'conn.guide.s3.note.buckets': 'A bucket is a filex storage. A client can create folders and objects inside one, but not the bucket itself — that needs a driver and a path, which an S3 request cannot express.',
573
+ 'conn.guide.s3.note.permissions': 'The key sees exactly what you see. A bucket you may not open answers "no such bucket" rather than "forbidden", so the endpoint never reveals what exists.',
574
+ 'conn.guide.s3.note.trash': 'A delete goes to the filex trash, the same as everywhere else, and is recoverable until the retention policy sweeps it.',
575
+ 'conn.guide.s3.note.mtime': 'Uploads carry their modification time, and the checksums clients send (Content-MD5, x-amz-checksum-*) are verified — a corrupted upload is refused rather than stored.',
576
+ 'conn.guide.s3.note.pathStyle': 'This install has no dedicated S3 host, so clients must be told to use path-style addressing. Without it a current SDK fails at DNS with an error that names neither filex nor the cause.',
577
+ 'conn.guide.s3.note.http': 'This endpoint is plain http://. Signatures still protect the request, but the objects travel in the clear. Put filex behind TLS before pointing a backup at it.',
578
+ // ── SFTP endpoint: keys and the guide ────────────────────────────
579
+ 'conn.sshkeys.title': 'SSH keys',
580
+ 'conn.sshkeys.lead': 'Register a public key and every SSH client — sftp, scp, WinSCP, FileZilla, rclone, sshfs — can connect without sending your password.',
581
+ 'conn.sshkeys.disabled': 'The SFTP endpoint is switched off on this server (FILEX_SFTP). A key registered here will not connect until an operator turns it back on.',
582
+ 'conn.sshkeys.cannotAdd': 'You are not signed in with an account that can register keys.',
583
+ 'conn.sshkeys.paste': 'Paste the contents of ~/.ssh/id_ed25519.pub',
584
+ 'conn.sshkeys.name': 'Name (optional — the key comment is used otherwise)',
585
+ 'conn.sshkeys.add': 'Add key',
586
+ 'conn.sshkeys.noCopyId': 'ssh-copy-id cannot work here: it appends to ~/.ssh/authorized_keys over a shell, and filex has none. This box is the way in.',
587
+ 'conn.sshkeys.col.name': 'Name',
588
+ 'conn.sshkeys.col.fingerprint': 'Fingerprint',
589
+ 'conn.sshkeys.col.added': 'Added',
590
+ 'conn.sshkeys.col.lastUsed': 'Last used',
591
+ 'conn.sshkeys.noName': '(no name)',
592
+ 'conn.sshkeys.neverUsed': 'never',
593
+ 'conn.sshkeys.enable': 'Enable',
594
+ 'conn.sshkeys.disable': 'Disable',
595
+ 'conn.sshkeys.remove': 'Remove',
596
+ 'conn.sshkeys.confirm': 'Sure?',
597
+ 'conn.sshkeys.empty': 'No keys yet. Without one, clients sign in with your account password.',
598
+ 'conn.guide.sftp.summary': 'SFTP makes filex reachable by anything that already speaks SSH — a backup job, a scanner, WinSCP, rclone, or a mounted folder.',
599
+ 'conn.guide.sftp.fact.host': 'Host',
600
+ 'conn.guide.sftp.fact.hostHint': 'The same machine as the web app, on a port of its own.',
601
+ 'conn.guide.sftp.fact.port': 'Port',
602
+ 'conn.guide.sftp.fact.portHint': 'Not 22 and not 443: SFTP is raw TCP and cannot go through the web proxy.',
603
+ 'conn.guide.sftp.fact.userHint': 'Your short login name. An @ in a login has to be quoted in most client config files, which is what this avoids.',
604
+ 'conn.guide.sftp.fact.auth': 'Sign in with',
605
+ 'conn.guide.sftp.fact.authKey': 'your registered SSH key (or your password)',
606
+ 'conn.guide.sftp.fact.authPassword': 'your account password — or register a key above',
607
+ 'conn.guide.sftp.fact.authHint': 'An API token works as a password too, and is revocable on its own. An account with two-factor sign-in must use a key or a token.',
608
+ 'conn.guide.sftp.fact.path': 'Path',
609
+ 'conn.guide.sftp.fact.pathHint': 'The first folder is a storage; the root lists the ones you may open.',
610
+ 'conn.guide.sftp.openssh.connectCaption': 'Connect, put, get',
611
+ 'conn.guide.sftp.openssh.noShell': 'There is no shell here — only the SFTP subsystem. `ssh host command` is refused on purpose, and scp works because OpenSSH 9 speaks SFTP for it.',
612
+ 'conn.guide.sftp.key.tab': 'Set up a key',
613
+ 'conn.guide.sftp.key.s1': 'Generate a key pair on the machine that will connect (skip if you already have one).',
614
+ 'conn.guide.sftp.key.s2': 'Copy the PUBLIC half — the .pub file, never the other one.',
615
+ 'conn.guide.sftp.key.s3': 'Paste it into the box above and connect; no password is sent from then on.',
616
+ 'conn.guide.sftp.key.genCaption': 'On your own machine',
617
+ 'conn.guide.sftp.key.noCopyId': 'ssh-copy-id will NOT work against filex: it needs a shell to append to ~/.ssh/authorized_keys, and there is none. Paste the key above instead.',
618
+ 'conn.guide.sftp.winscp.s1': 'New site → File protocol: SFTP.',
619
+ 'conn.guide.sftp.winscp.s2': 'Host name: {host}, port: {port}.',
620
+ 'conn.guide.sftp.winscp.s3': 'User name: {user}. For a key: Advanced → SSH → Authentication → private key file.',
621
+ 'conn.guide.sftp.winscp.s4': 'WinSCP needs a .ppk private key — use its own tool (Tools → PuTTYgen) to convert an OpenSSH key once.',
622
+ 'conn.guide.sftp.filezilla.s1': 'File → Site Manager → New site, Protocol: SFTP.',
623
+ 'conn.guide.sftp.filezilla.s2': 'Host: {host}, port: {port}.',
624
+ 'conn.guide.sftp.filezilla.s3': 'User: {user}. Logon type: Normal for a password, or Key file for a key.',
625
+ 'conn.guide.sftp.rclone.useCaption': 'Use it',
626
+ 'conn.guide.sftp.sshfs.mountCaption': 'Mount and unmount',
627
+ 'conn.guide.sftp.sshfs.note': 'A mounted folder rewrites the whole file on every save. Good for documents, not for a database or a video you are editing in place.',
628
+ 'conn.guide.sftp.note.storages': 'The root is not a home directory: it lists the storages you may open, and the first path segment names one.',
629
+ 'conn.guide.sftp.note.permissions': 'The permission bits your client draws come from your access level here, so a file shown as read-only really is one for you.',
630
+ 'conn.guide.sftp.note.trash': 'A delete goes to the filex trash, the same as everywhere else, and is recoverable until the retention policy sweeps it.',
631
+ 'conn.guide.sftp.note.totp': 'If your account has two-factor sign-in, the password will not work here — SSH has no way to ask for the code. Register a key, or use an API token as the password.',
632
+ 'conn.guide.sftp.note.disabled': 'The SFTP endpoint is switched off on this server. These instructions will work once an operator enables it.',
633
+ // ── FTPS ─────────────────────────────────────────────────────────
634
+ 'conn.guide.ftps.summary': 'FTPS is here for the equipment that only ever learned FTP — scan-to-folder printers, EDI counterparties, older lab and industrial software. Always over TLS.',
635
+ 'conn.guide.ftps.fact.host': 'Host',
636
+ 'conn.guide.ftps.fact.hostHint': 'The same machine as the web app, on a port of its own.',
637
+ 'conn.guide.ftps.fact.port': 'Port',
638
+ 'conn.guide.ftps.fact.portHint': 'The control channel. Data moves on a separate port from the passive range below.',
639
+ 'conn.guide.ftps.fact.mode': 'Encryption',
640
+ 'conn.guide.ftps.fact.modeValue': 'FTPS — explicit TLS (AUTH TLS), required',
641
+ 'conn.guide.ftps.fact.modeHint': 'Plain FTP is refused before the password is read. Choose "Require explicit FTP over TLS", never "plain FTP" and never "implicit".',
642
+ 'conn.guide.ftps.fact.userHint': 'Your short login name, or your e-mail. An API token works as the password too and can be revoked on its own.',
643
+ 'conn.guide.ftps.fact.passwordHint': 'Your account password, or an API token. An account with two-factor sign-in must use a token — FTP has no way to ask for the code.',
644
+ 'conn.guide.ftps.fact.pasv': 'Passive ports',
645
+ 'conn.guide.ftps.fact.pasvHint': 'Data connections land here. If your firewall blocks this range the transfer HANGS with no error on either side — that is the classic FTP failure.',
646
+ 'conn.guide.ftps.filezilla.s1': 'File → Site Manager → New site, Protocol: FTP.',
647
+ 'conn.guide.ftps.filezilla.s2': 'Host: {host}, port: {port}.',
648
+ 'conn.guide.ftps.filezilla.s3': 'Encryption: "Require explicit FTP over TLS". Logon type: Normal, user: {user}.',
649
+ 'conn.guide.ftps.filezilla.s4': 'Transfer settings → Passive. Active mode is refused by this server.',
650
+ 'conn.guide.ftps.winscp.s1': 'New site → File protocol: FTP, Encryption: TLS/SSL Explicit encryption.',
651
+ 'conn.guide.ftps.winscp.s2': 'Host name: {host}, port: {port}.',
652
+ 'conn.guide.ftps.winscp.s3': 'User name: {user}, password as above.',
653
+ 'conn.guide.ftps.curl.caption': 'Upload and download',
654
+ 'conn.guide.ftps.curl.sslReqd': '--ssl-reqd is not optional: it makes curl REQUIRE TLS instead of falling back to plaintext. This server refuses plaintext anyway, but the habit protects you against the servers that do not.',
655
+ 'conn.guide.ftps.lftp.caption': 'Connect',
656
+ 'conn.guide.ftps.lftp.protectData': 'ssl-protect-data encrypts the FILE as well as the login. Without it lftp logs in over TLS and then sends your file in the clear — which is the FTPS misconfiguration people actually ship.',
657
+ 'conn.guide.ftps.rclone.useCaption': 'Use it',
658
+ 'conn.guide.ftps.printer.tab': 'Scanner / printer',
659
+ 'conn.guide.ftps.printer.s1': 'In the device\'s scan-to-FTP settings: server {host}, port {port}.',
660
+ 'conn.guide.ftps.printer.s2': 'User: {user}, password as above. Create an API token for the device rather than using your own password — it can be revoked without changing anything else.',
661
+ 'conn.guide.ftps.printer.s3': 'Path: {path} — or a folder inside it that you created first. The device cannot create a storage.',
662
+ 'conn.guide.ftps.printer.s4': 'Turn ON "SSL/TLS" or "FTPS explicit", and PASSIVE mode.',
663
+ 'conn.guide.ftps.printer.noTLS': 'Plenty of scan-to-FTP firmware cannot do TLS at all. This server will not talk to such a device, and that is deliberate — the alternative is your documents and your password crossing the office network in the clear.',
664
+ 'conn.guide.ftps.note.tls': 'TLS is mandatory and there is no switch to turn it off. Plain FTP sends your password in the clear and your file after it.',
665
+ 'conn.guide.ftps.note.passive': 'Passive mode only. Active mode has the server dial back to the client, which does not survive NAT and is blocked by most firewalls.',
666
+ 'conn.guide.ftps.note.storages': 'The root is not a home directory: it lists the storages you may open, and the first path segment names one.',
667
+ 'conn.guide.ftps.note.trash': 'A delete goes to the filex trash, the same as everywhere else, and is recoverable until the retention policy sweeps it.',
668
+ 'conn.guide.ftps.note.prefer': 'If the client can speak SFTP, prefer it: one connection, one port, no passive-mode surprises.',
669
+ 'conn.guide.ftps.note.selfSigned': 'This server is using a self-signed certificate. It encrypts the channel but proves nothing about who is on the other end — most clients will ask you to accept it once. Supply a real certificate for anything that matters.',
670
+ 'conn.guide.ftps.note.disabled': 'The FTPS endpoint is switched off on this server. These instructions will work once an operator enables it.',
671
+ // ── NFS ──────────────────────────────────────────────────────────
672
+ 'conn.nfs.title': 'NFS exports',
673
+ 'conn.nfs.lead': 'An export lets a machine on your network mount filex as a drive — a media player, a build server, a backup box.',
674
+ 'conn.nfs.pathIsSecret': '⚠ The export path IS the password. NFS cannot ask a client who it is without Kerberos, so filex puts 32 random bytes in the path instead: whoever knows it can mount as you. Treat the mount line like a credential — /etc/fstab is world-readable on most systems.',
675
+ 'conn.nfs.disabled': 'The NFS endpoint is switched off on this server (FILEX_NFS). An export created here will not mount until an operator turns it back on.',
676
+ 'conn.nfs.cannotMint': 'You are not signed in with an account that can create exports.',
677
+ 'conn.nfs.label': 'What is it for? (media player, backup box, …)',
678
+ 'conn.nfs.defaultLabel': 'nfs export',
679
+ 'conn.nfs.everyStorage': 'Every storage I can see',
680
+ 'conn.nfs.prefix': 'Folder inside the storage (optional)',
681
+ 'conn.nfs.allowCidrs': 'Allowed addresses, e.g. 192.168.1.0/24 (optional)',
682
+ 'conn.nfs.readOnly': 'Read-only',
683
+ 'conn.nfs.readOnlyHint': 'Read-only is the safer default for a machine: it refuses every write through this mount whatever your own permissions are. An address list narrows it further — outside it, the mount is refused.',
684
+ 'conn.nfs.mint': 'Create export',
685
+ 'conn.nfs.once': 'Copy this now — the path is shown only once and cannot be recovered.',
686
+ 'conn.nfs.dismiss': 'I have copied it',
687
+ 'conn.nfs.col.label': 'Label',
688
+ 'conn.nfs.col.scope': 'Limited to',
689
+ 'conn.nfs.col.mode': 'Mode',
690
+ 'conn.nfs.col.lastUsed': 'Last mounted',
691
+ 'conn.nfs.scopeAll': 'everything you can see',
692
+ 'conn.nfs.modeRead': 'read-only',
693
+ 'conn.nfs.modeWrite': 'read/write',
694
+ 'conn.nfs.noLabel': '(no label)',
695
+ 'conn.nfs.neverUsed': 'never',
696
+ 'conn.nfs.enable': 'Enable',
697
+ 'conn.nfs.disable': 'Disable',
698
+ 'conn.nfs.revoke': 'Revoke',
699
+ 'conn.nfs.confirm': 'Sure?',
700
+ 'conn.nfs.empty': 'No exports yet.',
701
+ 'conn.guide.nfs.summary': 'NFS mounts filex as a drive on a machine in your own network — the NAS protocol, for the devices that expect one.',
702
+ 'conn.guide.nfs.pathPlaceholder': 'create an export above',
703
+ 'conn.guide.nfs.fact.host': 'Host',
704
+ 'conn.guide.nfs.fact.hostHint': 'The same machine as the web app, on a port of its own.',
705
+ 'conn.guide.nfs.fact.port': 'Port',
706
+ 'conn.guide.nfs.fact.portHint': 'Both port= and mountport= must be set to this; filex serves the mount and NFS services on one port.',
707
+ 'conn.guide.nfs.fact.export': 'Export path',
708
+ 'conn.guide.nfs.fact.exportHint': 'This is the credential. It is shown once, when you create the export, and is stored hashed — filex cannot show it again.',
709
+ 'conn.guide.nfs.fact.options': 'Mount options',
710
+ 'conn.guide.nfs.fact.optionsHint': 'nolock because filex does not run the NFS lock manager; nfsvers=3 because that is what this server speaks.',
711
+ 'conn.guide.nfs.linux.mountCaption': 'Mount it',
712
+ 'conn.guide.nfs.linux.fstabSecret': 'An /etc/fstab line containing this path is a password in a world-readable file. Use a credentials-protected automount, or keep the mount in a root-only unit, if that matters where you are.',
713
+ 'conn.guide.nfs.mac.mountCaption': 'Mount it',
714
+ 'conn.guide.nfs.mac.resvport': 'macOS needs resvport: its client insists on a privileged source port and refuses the mount without it.',
715
+ 'conn.guide.nfs.win.s1': 'Turn on "Services for NFS" → "Client for NFS" in Windows Features (Pro and Enterprise only; Home does not have it).',
716
+ 'conn.guide.nfs.win.s2': 'Open a Command Prompt as administrator.',
717
+ 'conn.guide.nfs.win.s3': 'Map the export to a drive letter with the command below.',
718
+ 'conn.guide.nfs.win.cmdCaption': 'Map it to Z:',
719
+ 'conn.guide.nfs.win.port': 'Windows has no way to say which port the mount service is on, so it can only reach an NFS server on the standard port 2049. If this server is on another port, use SFTP or the filex desktop app instead.',
720
+ 'conn.guide.nfs.nas.tab': 'NAS / media player',
721
+ 'conn.guide.nfs.nas.s1': 'In the device\'s NFS settings: server {host}, port {port} (set both the NFS and the mount port if it asks for them separately).',
722
+ 'conn.guide.nfs.nas.s2': 'Remote path: the export path above, exactly as shown.',
723
+ 'conn.guide.nfs.nas.s3': 'NFS version 3, no locking. Create the export read-only unless the device genuinely needs to write.',
724
+ 'conn.guide.nfs.note.unencrypted': 'NFSv3 is not encrypted. Anyone who can read the traffic sees your files, and anyone who learns the path can mount them. Use it on a home or office network, or over a VPN — never across the internet.',
725
+ 'conn.guide.nfs.note.pathIsSecret': 'The export path is the whole credential. Revoke it from the list above if it leaks; there is nothing else to change.',
726
+ 'conn.guide.nfs.note.noPortmapper': 'There is no portmapper on port 111, so a client that is not told the port cannot find the server. That is why every command here carries port= and mountport=.',
727
+ 'conn.guide.nfs.note.uid': 'The user and group ids your client sends are ignored: the mount already knows whose it is. The permissions you see come from your access in filex.',
728
+ 'conn.guide.nfs.note.trash': 'A delete goes to the filex trash, the same as everywhere else, and is recoverable until the retention policy sweeps it.',
729
+ 'conn.guide.nfs.note.revoke': 'Revoking takes effect on the next request, not instantly: NFS has no session to end, so a client with the mount open sees its next operation fail.',
730
+ 'conn.guide.nfs.note.disabled': 'The NFS endpoint is switched off on this server. These instructions will work once an operator enables it.',
731
+ 'conn.tokens.title': 'API tokens',
732
+ 'conn.tokens.lead': 'FTPS, WebDAV and filex mount all sign in with a token instead of your account password. Mint one here — it can be revoked on its own, and an account with two-factor sign-in cannot use its password on these at all.',
733
+ 'conn.tokens.mint': 'Create token',
734
+ 'conn.tokens.once': 'Copy it now — this is the only time it is shown. Only a hash is stored, so it cannot be shown again.',
735
+ 'conn.tokens.dismiss': 'I have copied it',
736
+ 'conn.tokens.cannotMint': 'This session cannot create tokens. Sign in with your own account to make one.',
737
+ 'conn.tokens.empty': 'No tokens yet.',
738
+ 'conn.tokens.neverUsed': 'never',
739
+ 'conn.tokens.revoke': 'Revoke',
740
+ 'conn.tokens.confirm': 'Sure?',
741
+ 'conn.tokens.col.label': 'Name',
742
+ 'conn.tokens.col.scopes': 'Can do',
743
+ 'conn.tokens.col.used': 'Last used',
744
+ 'conn.tokens.revokeHint': 'Revoking stops a connection that is already open, not just the next sign-in: an SFTP or FTPS session is cut and a mount stops working within about half a minute.',
745
+ 'conn.guide.mount.summary': 'filex mount attaches a remote filex server to a folder on this machine, over the same HTTPS the browser uses. It is the only one of these that works from anywhere — no LAN, no extra server, no third-party client.',
746
+ 'conn.guide.mount.fact.url': 'Server URL',
747
+ 'conn.guide.mount.fact.urlHint': 'The same address you use in the browser. Nothing else has to be reachable.',
748
+ 'conn.guide.mount.fact.token': 'API token',
749
+ 'conn.guide.mount.fact.tokenPlaceholder': 'create one under Tokens',
750
+ 'conn.guide.mount.fact.tokenHint': 'A token, not your password — it can be revoked on its own, and an account with 2FA cannot use its password here at all.',
751
+ 'conn.guide.mount.fact.remote': 'What to mount',
752
+ 'conn.guide.mount.fact.remoteHint': 'Leave --remote off to see every storage as a folder, or name one storage — or a folder inside it — to mount just that.',
753
+ 'conn.guide.mount.linux.mountCaption': 'Mount it',
754
+ 'conn.guide.mount.linux.umountCaption': 'Unmount it',
755
+ 'conn.guide.mount.linux.umountWarn': 'Unmount with fusermount, not by killing the process. A mount whose process died without detaching leaves a folder where every ls hangs until somebody runs fusermount by hand.',
756
+ 'conn.guide.mount.linux.systemdCaption': 'Mount it at login (systemd user unit)',
757
+ 'conn.guide.mount.win.winfsp': 'Windows needs WinFsp installed once — it is free and open source: https://winfsp.dev. Nothing else, and no administrator rights after that.',
758
+ 'conn.guide.mount.win.mountCaption': 'Mount it as a drive',
759
+ 'conn.guide.mount.win.freeLetter': 'Pick a drive letter that is FREE. The letter is created by the mount, not attached to something that already exists, and pointing it at one that is in use fails with a message that does not say so.',
760
+ 'conn.guide.mount.win.stop': 'Stop it with Ctrl-C in that window. The drive disappears and nothing is left behind on this machine.',
761
+ 'conn.guide.mount.mac.unsupported': 'filex mount does not work on macOS. It needs macFUSE, whose licence does not allow a program like filex to install it for you, and whose Go binding needs a C compiler filex deliberately does not require. The command refuses here rather than appearing to work and doing nothing.',
762
+ 'conn.guide.mount.mac.alternatives': 'On macOS, use the filex desktop app with folder sync (keeps the files on disk), or SFTP with a client that mounts drives.',
763
+ 'conn.guide.mount.note.notASync': 'This is not a sync. Nothing is copied to this machine except a small read cache, so a mount opens one file out of a hundred thousand without downloading the rest. If you want the files when you are offline, use folder sync instead.',
764
+ 'conn.guide.mount.note.reachable': 'It reaches the server through whatever proxy or tunnel the browser goes through, because underneath it is the same API. Your permissions, your storages and — on a multi-tenant server — your tenant come with it.',
765
+ 'conn.guide.mount.note.wholeFileWrites': 'A file you write through the mount is uploaded when the program closes it, not while it is being written. Editing a very large file in place is slower here than a local disk; copying it in and out is not.',
766
+ 'conn.guide.mount.note.trash': 'A delete goes to the filex trash, the same as everywhere else, and is recoverable until the retention policy sweeps it.',
767
+ 'conn.guide.mount.note.revoke': 'Revoke the token to stop a mount. It stops serving within half a minute, even if the mount is still attached.',
768
+ 'storages.driver.ftp': 'FTP / FTPS',
769
+ 'storages.driver.local': 'Local filesystem',
770
+ 'storages.driver.s3': 'S3 / Hetzner / MinIO',
771
+ 'storages.driver.sftp': 'SFTP',
772
+ 'storages.driver.smb': 'SMB / CIFS (NAS)',
773
+ 'storages.driver.webdav': 'WebDAV',
774
+ 'storages.fieldHelp.smbShare': 'The share name alone, without the server: `media`, not `\\\\nas\\media`.',
775
+ 'storages.fieldHelp.smbDomain': 'Only for a Windows domain account. Leave empty for a NAS.',
776
+ 'storages.fieldHelp.smbRoot': 'Sub-folder inside the share. Leave empty for the whole share.',
777
+ 'storages.fields.share': 'Share',
778
+ 'storages.fields.domain': 'Domain / workgroup',
779
+ 'storages.fields.dialTimeout': 'Connect timeout (seconds)',
780
+ 'storages.fieldHelp.disablePresign': 'Turn on when the store rejects SDK-signed URLs (Ceph RGW / some Hetzner setups answer SignatureDoesNotMatch); uploads then stream through the backend.',
781
+ 'storages.fieldHelp.endpoint': 'Leave empty for AWS S3. Any S3-compatible store needs its endpoint.',
782
+ 'storages.fieldHelp.hostKey': 'A single public key in authorized_keys / known_hosts line form.',
783
+ 'storages.fieldHelp.insecureSkipHostKey': 'Accepts any host key. Only for throwaway hosts.',
784
+ 'storages.fieldHelp.keyPath': 'Path to a key file on the server, read when the storage starts. Used when the PEM field is empty.',
785
+ 'storages.fieldHelp.knownHosts': 'OpenSSH known_hosts path for strict host-key checking. Default is trust-on-first-use in ~/.filex/known_hosts.',
786
+ 'storages.fieldHelp.passive': 'Off switches to active mode, which needs the server to dial back into this host.',
787
+ 'storages.fieldHelp.path': 'Directory on the server. It is created if missing; filex never mounts /.',
788
+ 'storages.fieldHelp.pathStyle': 'On for every non-AWS store. It is turned on automatically when an endpoint is set and this was never touched.',
789
+ 'storages.fieldHelp.prefix': 'Sub-folder inside the bucket. Required: filex never takes ownership of the bucket root.',
790
+ 'storages.fieldHelp.region': 'Defaults to "auto" when left empty.',
791
+ 'storages.fieldHelp.root': 'Sub-folder on the backend. Required: filex never mounts the account root.',
792
+ 'storages.fieldHelp.sftpAuth': 'Either a password or a private key is required.',
793
+ 'storages.fieldHelp.tls': 'Plain FTP sends credentials in the clear — turn this on whenever the server supports it.',
794
+ 'storages.fields.accessKey': 'Access key',
795
+ 'storages.fields.basePath': 'Base path',
796
+ 'storages.fields.bucket': 'Bucket',
797
+ 'storages.fields.disablePresign': 'Disable presigned URLs',
798
+ 'storages.fields.endpoint': 'Endpoint',
799
+ 'storages.fields.host': 'Host',
800
+ 'storages.fields.hostKey': 'Pinned host key',
801
+ 'storages.fields.insecureSkipHostKey': 'Skip host-key verification (insecure)',
802
+ 'storages.fields.keyPath': 'Private key file',
803
+ 'storages.fields.knownHosts': 'known_hosts file',
804
+ 'storages.fields.name': 'Display name',
805
+ 'storages.fields.namePlaceholder': 'Personal photos',
806
+ 'storages.fields.passive': 'Passive mode (PASV)',
807
+ 'storages.fields.password': 'Password',
808
+ 'storages.fields.path': 'Filesystem path',
809
+ 'storages.fields.pathStyle': 'Use path-style URLs (Hetzner, MinIO)',
810
+ 'storages.fields.port': 'Port',
811
+ 'storages.fields.prefix': 'Prefix',
812
+ 'storages.fields.privateKey': 'Private key (PEM)',
813
+ 'storages.fields.rbac': 'Per-item access control (RBAC)',
814
+ 'storages.fields.rbacHint': 'When on, non-admins see only files/folders explicitly granted to them via the permissions panel. When off, the storage is open to all users (capability by role).',
815
+ 'storages.fields.readOnly': 'Read-only mount',
816
+ 'storages.fields.region': 'Region',
817
+ 'storages.fields.root': 'Base path',
818
+ 'storages.fields.secretKey': 'Secret key',
819
+ 'storages.fields.tls': 'FTPS (explicit AUTH TLS)',
820
+ 'storages.fields.url': 'Base URL',
821
+ 'storages.fields.user': 'User',
822
+
410
823
  };