@_linked/server 2.4.0 → 2.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @\_linked/server
2
2
 
3
+ ## 2.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#44](https://github.com/linked-fw/server/pull/44) [`616f019`](https://github.com/linked-fw/server/commit/616f0195e0c263ccb6240d65c9c94435319649a0) Thanks [@flyon](https://github.com/flyon)! - Fix `LocalFileStore.saveFile`: preserve the key, honour `basePath`, and report where the file landed.
8
+
9
+ `saveFile` went through `getUploadTarget`, which lowercases the name and always resolves against the server's upload
10
+ folder. Two consequences: a key whose case matters (a Vite content hash such as `main-hwqwrAvA.css`) was corrupted, and
11
+ a store built with a custom `basePath` wrote to the upload folder while `getFile`, `fileExists`, `deleteFile` and
12
+ `statFile` all read from `basePath` — so it could never read back what it wrote.
13
+
14
+ `saveFile` now resolves the target against the store's own `basePath`, like every other method, and keeps the name's
15
+ case. Unsafe characters are still replaced and a missing extension is still added from `mimeType`; lowercasing stays
16
+ where it belongs, in `@_linked/server-utils`' upload handling, which already sanitises a browser-supplied filename
17
+ before any store sees it.
18
+
19
+ New `saveFileWithPath()` returns `{storedPath, publicURL}`, so a caller can hand `storedPath` straight to
20
+ `statFile`/`getFile`. With `preventDuplicates: false` that path equals the path given, verbatim.
21
+
22
+ Minor rather than patch because behaviour changes for existing callers in two ways:
23
+
24
+ - a name containing uppercase characters is now stored with its case intact instead of lowercased. Callers that pass an
25
+ already-lowercased name (which is what the HTTP upload path does) are unaffected.
26
+ - a store constructed with a custom `basePath` now writes into that folder instead of `./data/uploads`.
27
+
28
+ Unchanged: the default `basePath`, the suffixing default when `preventDuplicates` is unspecified, the returned URL, and
29
+ the two-argument `LinkedFileStorage.saveFile(path, buffer)` path.
30
+
3
31
  ## 2.4.0
4
32
 
5
33
  ### Minor Changes
@@ -1,5 +1,19 @@
1
1
  import { FileStat, IFileStore, SaveFileOptions } from '@_linked/core/interfaces/IFileStore';
2
2
  import { Shape } from '@_linked/core/shapes/Shape';
3
+ /**
4
+ * What `saveFile` stored, and where.
5
+ *
6
+ * `storedPath` is the key as it ended up in the store: it is exactly what
7
+ * `getFile`, `fileExists`, `deleteFile` and `statFile` expect, so a caller that
8
+ * wants to verify what it just uploaded never has to guess a name back out of a
9
+ * URL.
10
+ */
11
+ export interface SavedFileLocation {
12
+ /** The key the file is stored under, relative to the store's base folder. */
13
+ storedPath: string;
14
+ /** The public URL, the same string `saveFile` returns. */
15
+ publicURL: string;
16
+ }
3
17
  export declare class LocalFileStore extends Shape implements IFileStore {
4
18
  private readonly basePath;
5
19
  readonly accessURL: string;
@@ -32,24 +46,59 @@ export declare class LocalFileStore extends Shape implements IFileStore {
32
46
  * @todo Take a path parameter to list files in a subdirectory
33
47
  */
34
48
  listFiles(prefix?: string): Promise<string[]>;
49
+ /**
50
+ * Work out the key a save will use, and the URL that key is served under.
51
+ *
52
+ * This used to be `getUploadTarget` from `@_linked/server-utils`, which could
53
+ * not be used here for two reasons:
54
+ *
55
+ * - it lowercases the name. That is HTTP-upload hygiene (browsers hand over
56
+ * whatever the user's filesystem had, and `getUploadTarget` is still the
57
+ * right place for it — `uploadSingleFileFromFormData` calls it before it
58
+ * ever reaches a store), but it corrupts a key a program chose on purpose,
59
+ * such as a Vite content hash: `main-hwqwrAvA.css`.
60
+ * - it always resolves against `relativeFileSystemUploadPath`, ignoring this
61
+ * store's `basePath`, so a store with a custom base folder wrote where it
62
+ * could not read back.
63
+ *
64
+ * Characters that are not safe in a file name are still replaced, exactly as
65
+ * `getUploadTarget` does it — only the case is left alone.
66
+ */
67
+ private resolveTarget;
35
68
  /**
36
69
  * Save a file to the local filesystem
37
- * @param filePath The path to save the file to, relative to the base upload folder
70
+ * @param filePath The path to save the file to, relative to the base folder
38
71
  * @param fileContent The contents of the file as a buffer
39
72
  * @param options Save options, or a string read as the old positional mimeType.
40
73
  * A local file has no headers to attach, so only `mimeType` (used to add a
41
74
  * missing extension) and `preventDuplicates` mean anything here;
42
75
  * `cacheControl` and `metadata` are accepted and ignored.
43
76
  * @param preventDuplicates Only honoured when `options` is a string
44
- * @returns A promise that resolves to the public URL of the file
77
+ * @returns A promise that resolves to the public URL of the file. Use
78
+ * {@link saveFileWithPath} when you also need the key it was stored under.
45
79
  */
46
80
  saveFile(filePath: string, fileContent: Buffer, options?: SaveFileOptions | string, preventDuplicates?: boolean): Promise<string>;
81
+ /**
82
+ * Save a file and report both the key it was stored under and its URL.
83
+ *
84
+ * `saveFile` can only return one string, and `IFileStore` says that string is
85
+ * a URL — but a caller that wants to verify what it just wrote needs the
86
+ * *key*, which is what `statFile`, `getFile`, `fileExists` and `deleteFile`
87
+ * take. That is what `storedPath` is: hand it straight back to any of them.
88
+ *
89
+ * The key equals the `filePath` given verbatim — case included — as long as
90
+ * the name needs no sanitising, already has an extension, and
91
+ * `preventDuplicates` is `false`. With `preventDuplicates` left unspecified
92
+ * (this store's default, kept for upload callers) a random suffix is added,
93
+ * and `storedPath` is the only place that name is reported.
94
+ */
95
+ saveFileWithPath(filePath: string, fileContent: Buffer, options?: SaveFileOptions | string, preventDuplicates?: boolean): Promise<SavedFileLocation>;
47
96
  /**
48
97
  * Read back the metadata of a stored file, for verify-after-upload.
49
98
  *
50
- * The path is resolved exactly like getFile/fileExists/deleteFile do, against
51
- * the base upload folder, which by default is the same folder saveFile writes
52
- * into, so a file that was just saved can be stat'ed by its stored name.
99
+ * The path is resolved exactly like saveFile/getFile/fileExists/deleteFile
100
+ * do, against this store's base folder, so the `storedPath` that
101
+ * saveFileWithPath reports can be handed straight back here.
53
102
  *
54
103
  * The sha256 is computed on demand from the bytes: statFile is only called
55
104
  * during publish verification, not on every upload.
@@ -1,4 +1,5 @@
1
- import { relativeFileSystemUploadPath, getUploadTarget, } from '@_linked/server-utils/utils/Upload';
1
+ import { relativeFileSystemUploadPath } from '@_linked/server-utils/utils/Upload';
2
+ import { publicUploadPath } from '@_linked/server-utils/utils/ServerPaths';
2
3
  import { normalizeSaveFileOptions, } from '@_linked/core/interfaces/IFileStore';
3
4
  import { Shape } from '@_linked/core/shapes/Shape';
4
5
  import { createHash } from 'node:crypto';
@@ -76,38 +77,103 @@ export class LocalFileStore extends Shape {
76
77
  }));
77
78
  return allFiles.flat();
78
79
  }
80
+ /**
81
+ * Work out the key a save will use, and the URL that key is served under.
82
+ *
83
+ * This used to be `getUploadTarget` from `@_linked/server-utils`, which could
84
+ * not be used here for two reasons:
85
+ *
86
+ * - it lowercases the name. That is HTTP-upload hygiene (browsers hand over
87
+ * whatever the user's filesystem had, and `getUploadTarget` is still the
88
+ * right place for it — `uploadSingleFileFromFormData` calls it before it
89
+ * ever reaches a store), but it corrupts a key a program chose on purpose,
90
+ * such as a Vite content hash: `main-hwqwrAvA.css`.
91
+ * - it always resolves against `relativeFileSystemUploadPath`, ignoring this
92
+ * store's `basePath`, so a store with a custom base folder wrote where it
93
+ * could not read back.
94
+ *
95
+ * Characters that are not safe in a file name are still replaced, exactly as
96
+ * `getUploadTarget` does it — only the case is left alone.
97
+ */
98
+ resolveTarget(filePath, mimeType, suffixDuplicates) {
99
+ // same character class as getUploadTarget: alphanumerics, dot, underscore
100
+ // and the path separator survive, a run of anything else becomes one dash.
101
+ // Case is deliberately preserved.
102
+ let sanitisedName = filePath.replace(/[^A-Za-z0-9\._\/]+/gi, '-');
103
+ if (sanitisedName.indexOf('.') === -1) {
104
+ //auto add an extension if none is present, based on the mimetype
105
+ if (!mimeType) {
106
+ throw new Error('This file does not have an extension and no mimetype was provided');
107
+ }
108
+ sanitisedName = sanitisedName + '.' + mimeType.split('/')[1];
109
+ }
110
+ const ext = sanitisedName.split('.').pop();
111
+ const withoutExt = sanitisedName.substring(0, sanitisedName.length - (ext.length + 1));
112
+ // 6 character random string to avoid clobbering an existing file,
113
+ // e.g. report.pdf -> report_312acb.pdf
114
+ const randomStringSuffix = suffixDuplicates
115
+ ? '_' + Math.random().toString(36).substring(2, 8)
116
+ : '';
117
+ const storedPath = withoutExt + randomStringSuffix + '.' + ext;
118
+ return {
119
+ storedPath,
120
+ publicURL: this.accessURL + publicUploadPath + '/' + storedPath,
121
+ };
122
+ }
79
123
  /**
80
124
  * Save a file to the local filesystem
81
- * @param filePath The path to save the file to, relative to the base upload folder
125
+ * @param filePath The path to save the file to, relative to the base folder
82
126
  * @param fileContent The contents of the file as a buffer
83
127
  * @param options Save options, or a string read as the old positional mimeType.
84
128
  * A local file has no headers to attach, so only `mimeType` (used to add a
85
129
  * missing extension) and `preventDuplicates` mean anything here;
86
130
  * `cacheControl` and `metadata` are accepted and ignored.
87
131
  * @param preventDuplicates Only honoured when `options` is a string
88
- * @returns A promise that resolves to the public URL of the file
132
+ * @returns A promise that resolves to the public URL of the file. Use
133
+ * {@link saveFileWithPath} when you also need the key it was stored under.
89
134
  */
90
135
  async saveFile(filePath, fileContent, options, preventDuplicates) {
136
+ const { publicURL } = await this.saveFileWithPath(filePath, fileContent, options, preventDuplicates);
137
+ return publicURL;
138
+ }
139
+ /**
140
+ * Save a file and report both the key it was stored under and its URL.
141
+ *
142
+ * `saveFile` can only return one string, and `IFileStore` says that string is
143
+ * a URL — but a caller that wants to verify what it just wrote needs the
144
+ * *key*, which is what `statFile`, `getFile`, `fileExists` and `deleteFile`
145
+ * take. That is what `storedPath` is: hand it straight back to any of them.
146
+ *
147
+ * The key equals the `filePath` given verbatim — case included — as long as
148
+ * the name needs no sanitising, already has an extension, and
149
+ * `preventDuplicates` is `false`. With `preventDuplicates` left unspecified
150
+ * (this store's default, kept for upload callers) a random suffix is added,
151
+ * and `storedPath` is the only place that name is reported.
152
+ */
153
+ async saveFileWithPath(filePath, fileContent, options, preventDuplicates) {
91
154
  var _a;
92
155
  const normalized = normalizeSaveFileOptions(options, preventDuplicates);
93
156
  // Core reports an unspecified preventDuplicates as undefined, leaving the
94
- // default to each store. This one has always let getUploadTarget append its
95
- // random suffix, so only an explicit `false` turns that off.
157
+ // default to each store. This one has always appended a random suffix, so
158
+ // only an explicit `false` turns that off.
96
159
  const suffixDuplicates = (_a = normalized.preventDuplicates) !== null && _a !== void 0 ? _a : true;
97
- const { publicURL, targetFilePath } = getUploadTarget(filePath, normalized.mimeType, null, '', this.accessURL, suffixDuplicates);
160
+ const target = this.resolveTarget(filePath, normalized.mimeType, suffixDuplicates);
161
+ // resolved against basePath, exactly like every read method does, so what
162
+ // is written here can be read back by the same key
163
+ const targetFilePath = path.join(this.basePath, target.storedPath);
98
164
  //make sure the target folder exists
99
165
  if (!fsSync.existsSync(path.dirname(targetFilePath))) {
100
166
  fsSync.mkdirSync(path.dirname(targetFilePath), { recursive: true });
101
167
  }
102
168
  await fs.writeFile(targetFilePath, fileContent);
103
- return publicURL;
169
+ return target;
104
170
  }
105
171
  /**
106
172
  * Read back the metadata of a stored file, for verify-after-upload.
107
173
  *
108
- * The path is resolved exactly like getFile/fileExists/deleteFile do, against
109
- * the base upload folder, which by default is the same folder saveFile writes
110
- * into, so a file that was just saved can be stat'ed by its stored name.
174
+ * The path is resolved exactly like saveFile/getFile/fileExists/deleteFile
175
+ * do, against this store's base folder, so the `storedPath` that
176
+ * saveFileWithPath reports can be handed straight back here.
111
177
  *
112
178
  * The sha256 is computed on demand from the bytes: statFile is only called
113
179
  * during publish verification, not on every upload.
@@ -1 +1 @@
1
- {"version":3,"file":"LocalFileStore.js","sourceRoot":"","sources":["../../../../src/shapes/filestores/LocalFileStore.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,eAAe,GAChB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAIL,wBAAwB,GACzB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,MAAM,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,cAAe,SAAQ,KAAK;IAKvC,YACE,CAA0B,EAC1B,WAAmB,4BAA4B;QAE/C,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,oBAAoB,CAAC,EAAE,CAAC;YAC5D,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,CAAC,CAAC,CAAC;QACX,CAAC;QAED,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QACvC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED;;;;OAIG;IACH,UAAU,CAAC,QAAgB;QACzB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAExD,OAAO,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACH,UAAU,CAAC,QAAgB;QACzB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAEvD,OAAO,EAAE;aACN,MAAM,CAAC,WAAW,CAAC;aACnB,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;aAChB,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,QAAgB;QACtB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAErD,OAAO,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,SAAS,CAAC,MAAe;QAC7B,IAAI,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE5C,mBAAmB;QACnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAEhD,OAAO,EAAE;iBACN,IAAI,CAAC,QAAQ,CAAC;iBACd,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;gBACb,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;oBACvB,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAC3C,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAC/C,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACpB,CAAC;YACH,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACb,OAAO,CAAC,IAAI,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;gBAC5C,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CACH,CAAC;QAEF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,QAAQ,CACZ,QAAgB,EAChB,WAAmB,EACnB,OAAkC,EAClC,iBAA2B;;QAE3B,MAAM,UAAU,GAAG,wBAAwB,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;QACxE,0EAA0E;QAC1E,4EAA4E;QAC5E,6DAA6D;QAC7D,MAAM,gBAAgB,GAAG,MAAA,UAAU,CAAC,iBAAiB,mCAAI,IAAI,CAAC;QAE9D,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,eAAe,CACnD,QAAQ,EACR,UAAU,CAAC,QAAQ,EACnB,IAAI,EACJ,EAAE,EACF,IAAI,CAAC,SAAS,EACd,gBAAgB,CACjB,CAAC;QAEF,oCAAoC;QACpC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC;YACrD,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,MAAM,EAAE,CAAC,SAAS,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QAEhD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,QAAQ,CAAC,QAAgB;QAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAEtD,IAAI,IAAkB,CAAC;QACvB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEnE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACrC,CAAC;CACF"}
1
+ {"version":3,"file":"LocalFileStore.js","sourceRoot":"","sources":["../../../../src/shapes/filestores/LocalFileStore.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/server.git"
12
12
  },
13
- "version": "2.4.0",
13
+ "version": "2.5.0",
14
14
  "linkedPackage": true,
15
15
  "type": "module",
16
16
  "main": "lib/esm/index.js",
package/readme.md CHANGED
@@ -342,7 +342,8 @@ LinkedFileStorage.setDefaultStore(store);
342
342
  ```
343
343
 
344
344
  `accessURL` comes from `SITE_ROOT`, and every path you pass to `saveFile`, `getFile`, `fileExists`, `deleteFile` and
345
- `statFile` is relative to that base folder.
345
+ `statFile` is relative to that base folder. Note that the URL `saveFile` returns is only meaningful when the base folder
346
+ is the server's own upload folder — that is the only folder the server serves.
346
347
 
347
348
  Registering stores per purpose (`LinkedFileStorage.setStore`, `getStore`, `registerPurpose`) is core's concern — see
348
349
  [`@_linked/core`](https://github.com/linked-cm/core), `utils/LinkedFileStorage` and `interfaces/IFileStore`.
@@ -371,6 +372,33 @@ following it.
371
372
  await store.saveFile('report.pdf', fileBuffer, 'application/pdf', true);
372
373
  ```
373
374
 
375
+ ##### The key you pass is the key that is stored
376
+
377
+ Characters that are unsafe in a file name are still replaced with a dash, and a missing extension is still added from
378
+ `mimeType`, but the **case of the name is preserved**. A build asset named `main-hwqwrAvA.css` is stored under exactly
379
+ that name. (Lowercasing belongs to HTTP upload handling, where a browser hands over whatever the user's filesystem had —
380
+ `getUploadTarget`/`uploadSingleFileFromFormData` in `@_linked/server-utils` still do it there, before a store is ever
381
+ called.)
382
+
383
+ ##### Getting the stored key back: `saveFileWithPath`
384
+
385
+ `saveFile` returns a URL, but `statFile`, `getFile`, `fileExists` and `deleteFile` take a *key*. `saveFileWithPath`
386
+ returns both, so verify-after-upload never has to guess a name back out of a URL:
387
+
388
+ ```ts
389
+ const { storedPath, publicURL } = await store.saveFileWithPath(
390
+ 'main-hwqwrAvA.css',
391
+ fileBuffer,
392
+ { mimeType: 'text/css', preventDuplicates: false }
393
+ );
394
+
395
+ const stat = await store.statFile(storedPath); // always resolves
396
+ ```
397
+
398
+ With `preventDuplicates: false`, `storedPath` equals the path you passed, verbatim, as long as it needs no sanitising
399
+ and already has an extension. With `preventDuplicates` left unspecified the random suffix is added and `storedPath` is
400
+ the only place the resulting name is reported.
401
+
374
402
  ##### LocalFileStore suffixes by default
375
403
 
376
404
  `preventDuplicates` left unspecified means `true` **here**. A second save of the same name does **not** overwrite the