@_linked/server 2.3.1 → 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,48 @@
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
+
31
+ ## 2.4.0
32
+
33
+ ### Minor Changes
34
+
35
+ - [#38](https://github.com/linked-fw/server/pull/38) [`bf1e83c`](https://github.com/linked-fw/server/commit/bf1e83cdfc949baec88a645c2750479bc33f1637) Thanks [@flyon](https://github.com/flyon)! - `LocalFileStore` accepts the new `SaveFileOptions` third argument of `saveFile`
36
+ (still accepting a positional mime-type string) and gains `statFile()`, which
37
+ returns the size and a sha256 of a stored file, or `null` when it does not
38
+ exist, for verify-after-upload. A local file has no headers, so `cacheControl`
39
+ and `metadata` are accepted and ignored.
40
+
41
+ Core reports an unspecified `preventDuplicates` as `undefined` and applies no
42
+ default of its own, so this store keeps applying its own: a caller that says
43
+ nothing still gets the random file-name suffix, and only an explicit
44
+ `preventDuplicates: false` overwrites an existing name.
45
+
3
46
  ## 2.3.1
4
47
 
5
48
  ### Patch Changes
@@ -7,8 +7,8 @@ import fs from 'fs/promises';
7
7
  export * from './shapes/quadstores/BackendAPIStoreProvider.js';
8
8
  export default class LincdServerBackendProvider extends BackendProvider {
9
9
  async setupBeforeControllers() {
10
- if (!LinkedFileStorage.getDefaultDataset()) {
11
- LinkedFileStorage.setDefaultDataset(new LocalFileStore(process.env.NODE_ENV + '-filestore'));
10
+ if (!LinkedFileStorage.getDefaultStore()) {
11
+ LinkedFileStorage.setDefaultStore(new LocalFileStore(process.env.NODE_ENV + '-filestore'));
12
12
  }
13
13
  let fileSystemUploadPath = path.join('data', 'filestores');
14
14
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,cAAc,gDAAgD,CAAC;AAE/D,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,eAAe;IACrE,KAAK,CAAC,sBAAsB;QAC1B,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC3C,iBAAiB,CAAC,iBAAiB,CACjC,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,YAAY,CAAC,CACxD,CAAC;QACJ,CAAC;QACD,IAAI,oBAAoB,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,YAAY,CAAC,EAAE;gBAC5D,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,SAAS;QACP,OAAO,aAAa,EAAE,CAAC;IACzB,CAAC;CACF"}
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,cAAc,gDAAgD,CAAC;AAE/D,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,eAAe;IACrE,KAAK,CAAC,sBAAsB;QAC1B,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,EAAE,CAAC;YACzC,iBAAiB,CAAC,eAAe,CAC/B,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,YAAY,CAAC,CACxD,CAAC;QACJ,CAAC;QACD,IAAI,oBAAoB,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,YAAY,CAAC,EAAE;gBAC5D,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,SAAS;QACP,OAAO,aAAa,EAAE,CAAC;IACzB,CAAC;CACF"}
@@ -1,5 +1,19 @@
1
- import { IFileStore } from '@_linked/core/interfaces/IFileStore';
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,11 +46,65 @@ 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
- * @returns A promise that resolves to the public URL of the file
72
+ * @param options Save options, or a string read as the old positional mimeType.
73
+ * A local file has no headers to attach, so only `mimeType` (used to add a
74
+ * missing extension) and `preventDuplicates` mean anything here;
75
+ * `cacheControl` and `metadata` are accepted and ignored.
76
+ * @param preventDuplicates Only honoured when `options` is a string
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.
79
+ */
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>;
96
+ /**
97
+ * Read back the metadata of a stored file, for verify-after-upload.
98
+ *
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.
102
+ *
103
+ * The sha256 is computed on demand from the bytes: statFile is only called
104
+ * during publish verification, not on every upload.
105
+ *
106
+ * @param filePath The path to the file, relative to the base upload folder
107
+ * @returns The size and sha256 of the file, or null if it does not exist
40
108
  */
41
- saveFile(filePath: string, fileContent: Buffer, mimeType?: string): Promise<string>;
109
+ statFile(filePath: string): Promise<FileStat | null>;
42
110
  }
@@ -1,5 +1,8 @@
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';
3
+ import { normalizeSaveFileOptions, } from '@_linked/core/interfaces/IFileStore';
2
4
  import { Shape } from '@_linked/core/shapes/Shape';
5
+ import { createHash } from 'node:crypto';
3
6
  import * as fsSync from 'node:fs';
4
7
  import fs from 'node:fs/promises';
5
8
  import path from 'node:path';
@@ -74,20 +77,125 @@ export class LocalFileStore extends Shape {
74
77
  }));
75
78
  return allFiles.flat();
76
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
+ }
77
123
  /**
78
124
  * Save a file to the local filesystem
79
- * @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
80
126
  * @param fileContent The contents of the file as a buffer
81
- * @returns A promise that resolves to the public URL of the file
127
+ * @param options Save options, or a string read as the old positional mimeType.
128
+ * A local file has no headers to attach, so only `mimeType` (used to add a
129
+ * missing extension) and `preventDuplicates` mean anything here;
130
+ * `cacheControl` and `metadata` are accepted and ignored.
131
+ * @param preventDuplicates Only honoured when `options` is a string
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.
134
+ */
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.
82
152
  */
83
- async saveFile(filePath, fileContent, mimeType) {
84
- const { publicURL, targetFilePath } = getUploadTarget(filePath, mimeType, null, '', this.accessURL);
153
+ async saveFileWithPath(filePath, fileContent, options, preventDuplicates) {
154
+ var _a;
155
+ const normalized = normalizeSaveFileOptions(options, preventDuplicates);
156
+ // Core reports an unspecified preventDuplicates as undefined, leaving the
157
+ // default to each store. This one has always appended a random suffix, so
158
+ // only an explicit `false` turns that off.
159
+ const suffixDuplicates = (_a = normalized.preventDuplicates) !== null && _a !== void 0 ? _a : true;
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);
85
164
  //make sure the target folder exists
86
165
  if (!fsSync.existsSync(path.dirname(targetFilePath))) {
87
166
  fsSync.mkdirSync(path.dirname(targetFilePath), { recursive: true });
88
167
  }
89
168
  await fs.writeFile(targetFilePath, fileContent);
90
- return publicURL;
169
+ return target;
170
+ }
171
+ /**
172
+ * Read back the metadata of a stored file, for verify-after-upload.
173
+ *
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.
177
+ *
178
+ * The sha256 is computed on demand from the bytes: statFile is only called
179
+ * during publish verification, not on every upload.
180
+ *
181
+ * @param filePath The path to the file, relative to the base upload folder
182
+ * @returns The size and sha256 of the file, or null if it does not exist
183
+ */
184
+ async statFile(filePath) {
185
+ const fileToStat = path.join(this.basePath, filePath);
186
+ let stat;
187
+ try {
188
+ stat = await fs.stat(fileToStat);
189
+ }
190
+ catch (err) {
191
+ return null;
192
+ }
193
+ if (!stat.isFile()) {
194
+ return null;
195
+ }
196
+ const contents = await fs.readFile(fileToStat);
197
+ const sha256 = createHash('sha256').update(contents).digest('hex');
198
+ return { size: stat.size, sha256 };
91
199
  }
92
200
  }
93
201
  //# sourceMappingURL=LocalFileStore.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LocalFileStore.js","sourceRoot":"","sources":["../../../../src/shapes/filestores/LocalFileStore.ts"],"names":[],"mappings":"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"}
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.3.1",
13
+ "version": "2.5.0",
14
14
  "linkedPackage": true,
15
15
  "type": "module",
16
16
  "main": "lib/esm/index.js",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@_linked/cli": "^1.2",
38
- "@_linked/core": "^2.18.1",
38
+ "@_linked/core": "^2.20.0",
39
39
  "@_linked/server-utils": "^1.2.0",
40
40
  "adm-zip": "^0.5.16",
41
41
  "archiver": "^7.0.1",
package/readme.md CHANGED
@@ -319,8 +319,8 @@ shape.
319
319
 
320
320
  ### LocalFileStore
321
321
 
322
- This is the default file store for LINCD. It is a simple file store that stores files in a local directory. Its usage
323
- follows the general pattern of other file stores in LINCD:
322
+ An `IFileStore` that stores files in a local directory. Its usage follows the general pattern of other file stores in
323
+ LINCD:
324
324
 
325
325
  - Define all storage locations when the server or application starts up
326
326
  - Access methods of the file store through the `LinkedFileStorage` class
@@ -330,15 +330,99 @@ It's important to note that this file store is not suitable for frontend usage -
330
330
  environment that has access to the [`fs` module](https://nodejs.org/api/fs.html).
331
331
 
332
332
  ```ts
333
- import { LinkedFileStorage } from '@_linked/core/lib/utils/LinkedFileStorage';
334
- import { LocalFileStore } from 'lincd-server/lib/shapes/LocalFileStore';
333
+ import { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';
334
+ import { LocalFileStore } from '@_linked/server/shapes/filestores/LocalFileStore';
335
335
  import path from 'path';
336
336
 
337
+ // the second argument is the base folder on disk; it defaults to the server's
338
+ // upload folder (`./data/uploads`, relative to the working directory)
337
339
  const pathToStore = path.join(process.cwd(), 'my-file-store');
338
340
  const store = new LocalFileStore('my-file-store', pathToStore);
339
341
  LinkedFileStorage.setDefaultStore(store);
340
342
  ```
341
343
 
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. 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.
347
+
348
+ Registering stores per purpose (`LinkedFileStorage.setStore`, `getStore`, `registerPurpose`) is core's concern — see
349
+ [`@_linked/core`](https://github.com/linked-cm/core), `utils/LinkedFileStorage` and `interfaces/IFileStore`.
350
+
351
+ #### Saving files
352
+
353
+ `saveFile` takes core's `SaveFileOptions` as its third argument:
354
+
355
+ ```ts
356
+ await store.saveFile('report.pdf', fileBuffer, {
357
+ mimeType: 'application/pdf',
358
+ preventDuplicates: true,
359
+ });
360
+ ```
361
+
362
+ A local file has no headers to attach, so only two of the options do anything here:
363
+
364
+ - `mimeType` — used to add a missing file extension.
365
+ - `preventDuplicates` — see below.
366
+ - `cacheControl` and `metadata` are accepted (so the same call works against an S3-backed store) and ignored.
367
+
368
+ The old positional form still works: a plain string third argument is read as the mime type, with `preventDuplicates`
369
+ following it.
370
+
371
+ ```ts
372
+ await store.saveFile('report.pdf', fileBuffer, 'application/pdf', true);
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
+
402
+ ##### LocalFileStore suffixes by default
403
+
404
+ `preventDuplicates` left unspecified means `true` **here**. A second save of the same name does **not** overwrite the
405
+ first: the store appends a random suffix, so `report.pdf` becomes `report_a1b2c3.pdf`. Only an explicit `false`
406
+ overwrites.
407
+
408
+ Core supplies no default of its own — an unspecified `preventDuplicates` reaches the store as `undefined` and each store
409
+ decides. Do not assume this store's choice holds elsewhere: `S3FileStore` in `@_linked/s3` overwrites by default.
410
+
411
+ #### Reading file metadata
412
+
413
+ `statFile(filePath)` returns `{size, sha256}` for a stored file, or `null` when it does not exist (or is not a regular
414
+ file). The `sha256` is a hex digest computed from the file's bytes on demand, so a caller can verify an upload it just
415
+ made:
416
+
417
+ ```ts
418
+ const stat = await store.statFile('report.pdf');
419
+ if (stat === null) throw new Error('upload went missing');
420
+ if (stat.sha256 !== expectedSha256) throw new Error('content mismatch');
421
+ ```
422
+
423
+ `statFile` is optional on `IFileStore`, so a caller holding an `IFileStore` rather than a `LocalFileStore` must check
424
+ that it exists before calling it.
425
+
342
426
  ## TODO
343
427
 
344
428
  - [ ] Add tests