@_linked/server 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
package-lock.json linguist-generated=true -diff merge=ours
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @\_linked/server
|
|
2
2
|
|
|
3
|
+
## 2.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#39](https://github.com/linked-fw/server/pull/39) [`dfdc27b`](https://github.com/linked-fw/server/commit/dfdc27b8a09f127d553c08a0e86ec67da784183c) Thanks [@flyon](https://github.com/flyon)! - Declare npm as the package manager for this repo and mark `package-lock.json` as a generated file.
|
|
8
|
+
|
|
9
|
+
## 2.5.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#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.
|
|
14
|
+
|
|
15
|
+
`saveFile` went through `getUploadTarget`, which lowercases the name and always resolves against the server's upload
|
|
16
|
+
folder. Two consequences: a key whose case matters (a Vite content hash such as `main-hwqwrAvA.css`) was corrupted, and
|
|
17
|
+
a store built with a custom `basePath` wrote to the upload folder while `getFile`, `fileExists`, `deleteFile` and
|
|
18
|
+
`statFile` all read from `basePath` — so it could never read back what it wrote.
|
|
19
|
+
|
|
20
|
+
`saveFile` now resolves the target against the store's own `basePath`, like every other method, and keeps the name's
|
|
21
|
+
case. Unsafe characters are still replaced and a missing extension is still added from `mimeType`; lowercasing stays
|
|
22
|
+
where it belongs, in `@_linked/server-utils`' upload handling, which already sanitises a browser-supplied filename
|
|
23
|
+
before any store sees it.
|
|
24
|
+
|
|
25
|
+
New `saveFileWithPath()` returns `{storedPath, publicURL}`, so a caller can hand `storedPath` straight to
|
|
26
|
+
`statFile`/`getFile`. With `preventDuplicates: false` that path equals the path given, verbatim.
|
|
27
|
+
|
|
28
|
+
Minor rather than patch because behaviour changes for existing callers in two ways:
|
|
29
|
+
|
|
30
|
+
- a name containing uppercase characters is now stored with its case intact instead of lowercased. Callers that pass an
|
|
31
|
+
already-lowercased name (which is what the HTTP upload path does) are unaffected.
|
|
32
|
+
- a store constructed with a custom `basePath` now writes into that folder instead of `./data/uploads`.
|
|
33
|
+
|
|
34
|
+
Unchanged: the default `basePath`, the suffixing default when `preventDuplicates` is unspecified, the returned URL, and
|
|
35
|
+
the two-argument `LinkedFileStorage.saveFile(path, buffer)` path.
|
|
36
|
+
|
|
3
37
|
## 2.4.0
|
|
4
38
|
|
|
5
39
|
### 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
|
|
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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
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
|
|
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
|
|
95
|
-
//
|
|
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
|
|
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
|
|
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
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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,
|
|
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,8 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/linked-fw/server.git"
|
|
12
12
|
},
|
|
13
|
-
"version": "2.
|
|
13
|
+
"version": "2.5.1",
|
|
14
|
+
"packageManager": "npm@11.19.1",
|
|
14
15
|
"linkedPackage": true,
|
|
15
16
|
"type": "module",
|
|
16
17
|
"main": "lib/esm/index.js",
|
|
@@ -19,9 +20,9 @@
|
|
|
19
20
|
"scripts": {
|
|
20
21
|
"start": "npm exec linked dev",
|
|
21
22
|
"test": "NODE_OPTIONS=--experimental-vm-modules npx jest --config jest.config.cjs",
|
|
22
|
-
"build": "rimraf ./lib &&
|
|
23
|
-
"build-esm": "echo \"compiling ESM\" && rimraf ./lib/esm &&
|
|
24
|
-
"copy-to-lib": "echo 'copying CSS + JSON assets to lib folder' &&
|
|
23
|
+
"build": "rimraf ./lib && npm run build-esm && npm run copy-to-lib",
|
|
24
|
+
"build-esm": "echo \"compiling ESM\" && rimraf ./lib/esm && tsc -p tsconfig-esm.json",
|
|
25
|
+
"copy-to-lib": "echo 'copying CSS + JSON assets to lib folder' && copyfiles -u 1 'src/**/*.css' 'src/**/*.json' lib/esm"
|
|
25
26
|
},
|
|
26
27
|
"keywords": [
|
|
27
28
|
"lincd",
|
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
|