@_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 +43 -0
- package/lib/esm/backend.js +2 -2
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/shapes/filestores/LocalFileStore.d.ts +72 -4
- package/lib/esm/shapes/filestores/LocalFileStore.js +114 -6
- package/lib/esm/shapes/filestores/LocalFileStore.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +88 -4
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
|
package/lib/esm/backend.js
CHANGED
|
@@ -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.
|
|
11
|
-
LinkedFileStorage.
|
|
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 {
|
package/lib/esm/backend.js.map
CHANGED
|
@@ -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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
109
|
+
statFile(filePath: string): Promise<FileStat | null>;
|
|
42
110
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { relativeFileSystemUploadPath
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
84
|
-
|
|
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
|
|
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":"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,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/linked-fw/server.git"
|
|
12
12
|
},
|
|
13
|
-
"version": "2.
|
|
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.
|
|
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
|
-
|
|
323
|
-
|
|
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/
|
|
334
|
-
import { LocalFileStore } from '
|
|
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
|