@fgv/ts-http-storage 5.1.0-5 → 5.1.0-50
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/LICENSE +21 -0
- package/dist/ts-http-storage.d.ts +74 -3
- package/dist/tsdoc-metadata.json +1 -1
- package/eslint.config.js +16 -0
- package/lib/packlets/storage/converters.d.ts +11 -1
- package/lib/packlets/storage/converters.d.ts.map +1 -1
- package/lib/packlets/storage/converters.js +17 -2
- package/lib/packlets/storage/converters.js.map +1 -1
- package/lib/packlets/storage/fsProvider.d.ts +2 -2
- package/lib/packlets/storage/fsProvider.d.ts.map +1 -1
- package/lib/packlets/storage/fsProvider.js +17 -1
- package/lib/packlets/storage/fsProvider.js.map +1 -1
- package/lib/packlets/storage/model.d.ts +58 -1
- package/lib/packlets/storage/model.d.ts.map +1 -1
- package/lib/packlets/storage/model.js.map +1 -1
- package/lib/packlets/storage/routes.d.ts.map +1 -1
- package/lib/packlets/storage/routes.js +2 -1
- package/lib/packlets/storage/routes.js.map +1 -1
- package/lib/packlets/storage/service.d.ts +2 -2
- package/lib/packlets/storage/service.d.ts.map +1 -1
- package/lib/packlets/storage/service.js +1 -1
- package/lib/packlets/storage/service.js.map +1 -1
- package/package.json +5 -5
- package/.rush/temp/4f4782db16a13ce96c2b268916d3e8a461b96fcf.tar.log +0 -61
- package/.rush/temp/chunked-rush-logs/ts-http-storage.build.chunks.jsonl +0 -9
- package/.rush/temp/operation/build/all.log +0 -9
- package/.rush/temp/operation/build/log-chunks.jsonl +0 -9
- package/.rush/temp/operation/build/state.json +0 -3
- package/.rush/temp/shrinkwrap-deps.json +0 -656
- package/config/api-extractor.json +0 -36
- package/config/rig.json +0 -10
- package/config/typedoc.json +0 -7
- package/docs/README.md +0 -298
- package/docs/classes/FsStorageProvider.createDirectory.md +0 -20
- package/docs/classes/FsStorageProvider.deleteFile.md +0 -20
- package/docs/classes/FsStorageProvider.getChildren.md +0 -20
- package/docs/classes/FsStorageProvider.getFile.md +0 -20
- package/docs/classes/FsStorageProvider.getItem.md +0 -20
- package/docs/classes/FsStorageProvider.md +0 -147
- package/docs/classes/FsStorageProvider.saveFile.md +0 -22
- package/docs/classes/FsStorageProvider.sync.md +0 -13
- package/docs/classes/FsStorageProviderFactory.forNamespace.md +0 -20
- package/docs/classes/FsStorageProviderFactory.md +0 -69
- package/docs/classes/HttpStorageService.createDirectory.md +0 -20
- package/docs/classes/HttpStorageService.deleteFile.md +0 -20
- package/docs/classes/HttpStorageService.getChildren.md +0 -20
- package/docs/classes/HttpStorageService.getFile.md +0 -20
- package/docs/classes/HttpStorageService.getItem.md +0 -20
- package/docs/classes/HttpStorageService.md +0 -145
- package/docs/classes/HttpStorageService.saveFile.md +0 -20
- package/docs/classes/HttpStorageService.sync.md +0 -20
- package/docs/functions/createStorageRoutes.md +0 -11
- package/docs/functions/normalizeRequestPath.md +0 -11
- package/docs/functions/sanitizeNamespace.md +0 -11
- package/docs/interfaces/ICreateStorageRoutesOptions.logger.md +0 -9
- package/docs/interfaces/ICreateStorageRoutesOptions.md +0 -61
- package/docs/interfaces/ICreateStorageRoutesOptions.providers.md +0 -9
- package/docs/interfaces/IFsStorageProviderFactoryOptions.md +0 -44
- package/docs/interfaces/IFsStorageProviderFactoryOptions.rootPath.md +0 -9
- package/docs/interfaces/IHttpStorageProvider.createDirectory.md +0 -20
- package/docs/interfaces/IHttpStorageProvider.deleteFile.md +0 -20
- package/docs/interfaces/IHttpStorageProvider.getChildren.md +0 -20
- package/docs/interfaces/IHttpStorageProvider.getFile.md +0 -20
- package/docs/interfaces/IHttpStorageProvider.getItem.md +0 -20
- package/docs/interfaces/IHttpStorageProvider.md +0 -115
- package/docs/interfaces/IHttpStorageProvider.saveFile.md +0 -22
- package/docs/interfaces/IHttpStorageProvider.sync.md +0 -13
- package/docs/interfaces/IHttpStorageProviderFactory.forNamespace.md +0 -20
- package/docs/interfaces/IHttpStorageProviderFactory.md +0 -36
- package/docs/interfaces/IStorageFileResponse.contentType.md +0 -9
- package/docs/interfaces/IStorageFileResponse.contents.md +0 -9
- package/docs/interfaces/IStorageFileResponse.md +0 -78
- package/docs/interfaces/IStorageFileResponse.path.md +0 -9
- package/docs/interfaces/IStoragePathRequest.md +0 -61
- package/docs/interfaces/IStoragePathRequest.namespace.md +0 -9
- package/docs/interfaces/IStoragePathRequest.path.md +0 -9
- package/docs/interfaces/IStorageSyncRequest.md +0 -44
- package/docs/interfaces/IStorageSyncRequest.namespace.md +0 -9
- package/docs/interfaces/IStorageSyncResponse.md +0 -44
- package/docs/interfaces/IStorageSyncResponse.synced.md +0 -9
- package/docs/interfaces/IStorageTreeChildrenResponse.children.md +0 -9
- package/docs/interfaces/IStorageTreeChildrenResponse.md +0 -61
- package/docs/interfaces/IStorageTreeChildrenResponse.path.md +0 -9
- package/docs/interfaces/IStorageTreeItem.md +0 -78
- package/docs/interfaces/IStorageTreeItem.name.md +0 -9
- package/docs/interfaces/IStorageTreeItem.path.md +0 -9
- package/docs/interfaces/IStorageTreeItem.type.md +0 -9
- package/docs/interfaces/IStorageWriteFileRequest.contentType.md +0 -9
- package/docs/interfaces/IStorageWriteFileRequest.contents.md +0 -9
- package/docs/interfaces/IStorageWriteFileRequest.md +0 -97
- package/docs/type-aliases/StorageItemType.md +0 -11
- package/docs/type-aliases/StorageNamespace.md +0 -11
- package/docs/variables/storageFileResponse.md +0 -9
- package/docs/variables/storagePathRequest.md +0 -9
- package/docs/variables/storageSyncRequest.md +0 -9
- package/docs/variables/storageTreeChildrenResponse.md +0 -9
- package/docs/variables/storageTreeItem.md +0 -9
- package/docs/variables/storageWriteFileRequest.md +0 -9
- package/etc/ts-http-storage.api.md +0 -184
- package/lib/test/unit/storage/fsProvider.test.d.ts +0 -2
- package/lib/test/unit/storage/fsProvider.test.d.ts.map +0 -1
- package/lib/test/unit/storage/fsProvider.test.js +0 -106
- package/lib/test/unit/storage/fsProvider.test.js.map +0 -1
- package/lib/test/unit/storage/routes.test.d.ts +0 -2
- package/lib/test/unit/storage/routes.test.d.ts.map +0 -1
- package/lib/test/unit/storage/routes.test.js +0 -124
- package/lib/test/unit/storage/routes.test.js.map +0 -1
- package/lib/test/unit/storage/service.test.d.ts +0 -2
- package/lib/test/unit/storage/service.test.d.ts.map +0 -1
- package/lib/test/unit/storage/service.test.js +0 -67
- package/lib/test/unit/storage/service.test.js.map +0 -1
- package/rush-logs/ts-http-storage.build.cache.log +0 -3
- package/rush-logs/ts-http-storage.build.log +0 -9
- package/src/index.ts +0 -28
- package/src/packlets/storage/converters.ts +0 -100
- package/src/packlets/storage/fsProvider.ts +0 -263
- package/src/packlets/storage/index.ts +0 -33
- package/src/packlets/storage/model.ts +0 -113
- package/src/packlets/storage/provider.ts +0 -33
- package/src/packlets/storage/routes.ts +0 -215
- package/src/packlets/storage/service.ts +0 -114
- package/src/test/unit/storage/fsProvider.test.ts +0 -129
- package/src/test/unit/storage/routes.test.ts +0 -165
- package/src/test/unit/storage/service.test.ts +0 -95
- package/temp/build/typescript/ts_l9Fw4VUO.json +0 -1
- package/temp/ts-http-storage.api.json +0 -3103
- package/temp/ts-http-storage.api.md +0 -184
- package/tsconfig.json +0 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erik Fortune
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -23,7 +23,7 @@ export declare class FsStorageProvider implements IHttpStorageProvider {
|
|
|
23
23
|
constructor(rootPath: string);
|
|
24
24
|
getItem(itemPath: string): Promise<Result<IStorageTreeItem>>;
|
|
25
25
|
getChildren(itemPath: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>>;
|
|
26
|
-
getFile(itemPath: string): Promise<Result<IStorageFileResponse>>;
|
|
26
|
+
getFile(itemPath: string, encoding?: StorageContentEncoding): Promise<Result<IStorageFileResponse>>;
|
|
27
27
|
saveFile(itemPath: string, contents: string, contentType?: string): Promise<Result<IStorageFileResponse>>;
|
|
28
28
|
deleteFile(itemPath: string): Promise<Result<boolean>>;
|
|
29
29
|
createDirectory(itemPath: string): Promise<Result<IStorageTreeItem>>;
|
|
@@ -51,7 +51,7 @@ export declare class HttpStorageService {
|
|
|
51
51
|
constructor(providers: IHttpStorageProviderFactory);
|
|
52
52
|
getItem(request: IStoragePathRequest): Promise<Result<IStorageTreeItem>>;
|
|
53
53
|
getChildren(request: IStoragePathRequest): Promise<Result<IStorageTreeChildrenResponse>>;
|
|
54
|
-
getFile(request:
|
|
54
|
+
getFile(request: IStorageReadFileRequest): Promise<Result<IStorageFileResponse>>;
|
|
55
55
|
saveFile(request: IStorageWriteFileRequest): Promise<Result<IStorageFileResponse>>;
|
|
56
56
|
deleteFile(request: IStoragePathRequest): Promise<Result<boolean>>;
|
|
57
57
|
createDirectory(request: IStoragePathRequest): Promise<Result<IStorageTreeItem>>;
|
|
@@ -84,7 +84,15 @@ export declare interface IFsStorageProviderFactoryOptions {
|
|
|
84
84
|
export declare interface IHttpStorageProvider {
|
|
85
85
|
getItem(path: string): Promise<Result<IStorageTreeItem>>;
|
|
86
86
|
getChildren(path: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>>;
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Reads a file.
|
|
89
|
+
* @param path - Path of the file to read.
|
|
90
|
+
* @param encoding - Preferred wire encoding for the returned `contents`. A
|
|
91
|
+
* provider that does not implement the requested encoding MUST answer in
|
|
92
|
+
* `'utf8'` and report that on the response, rather than failing — the field is
|
|
93
|
+
* a preference and the response is the authority.
|
|
94
|
+
*/
|
|
95
|
+
getFile(path: string, encoding?: StorageContentEncoding): Promise<Result<IStorageFileResponse>>;
|
|
88
96
|
saveFile(path: string, contents: string, contentType?: string): Promise<Result<IStorageFileResponse>>;
|
|
89
97
|
deleteFile(path: string): Promise<Result<boolean>>;
|
|
90
98
|
createDirectory(path: string): Promise<Result<IStorageTreeItem>>;
|
|
@@ -107,6 +115,19 @@ export declare interface IStorageFileResponse {
|
|
|
107
115
|
readonly path: string;
|
|
108
116
|
readonly contents: string;
|
|
109
117
|
readonly contentType?: string;
|
|
118
|
+
/**
|
|
119
|
+
* How `contents` is encoded. Absent means `'utf8'`.
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* **This field describes what the server actually produced, not what the
|
|
123
|
+
* client asked for**, and a client must branch on it rather than on its own
|
|
124
|
+
* request. A provider that does not implement `'base64'` ignores the request
|
|
125
|
+
* and answers with UTF-8 text; a client that base64-decoded on the strength of
|
|
126
|
+
* having *asked* would corrupt every such response. Trusting the field instead
|
|
127
|
+
* makes an older or simpler provider degrade to today's behavior rather than
|
|
128
|
+
* to garbage.
|
|
129
|
+
*/
|
|
130
|
+
readonly encoding?: StorageContentEncoding;
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
/**
|
|
@@ -118,6 +139,27 @@ export declare interface IStoragePathRequest {
|
|
|
118
139
|
readonly namespace?: StorageNamespace;
|
|
119
140
|
}
|
|
120
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Request for reading a file, with an optional preferred content encoding.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* `encoding` is a *preference*, not a demand: a provider that cannot honour it
|
|
147
|
+
* answers in `'utf8'` and says so on {@link IStorageFileResponse.encoding}. The
|
|
148
|
+
* response is the authority.
|
|
149
|
+
* @public
|
|
150
|
+
*/
|
|
151
|
+
export declare interface IStorageReadFileRequest extends IStoragePathRequest {
|
|
152
|
+
/**
|
|
153
|
+
* Preferred wire encoding for the response's `contents`. Absent means `'utf8'`.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* A preference, not a demand — see {@link IStorageFileResponse.encoding}, which
|
|
157
|
+
* reports what the server actually produced and is the field a client must
|
|
158
|
+
* branch on.
|
|
159
|
+
*/
|
|
160
|
+
readonly encoding?: StorageContentEncoding;
|
|
161
|
+
}
|
|
162
|
+
|
|
121
163
|
/**
|
|
122
164
|
* Request for sync operation.
|
|
123
165
|
* @public
|
|
@@ -174,6 +216,29 @@ export declare function normalizeRequestPath(requestPath: string): string;
|
|
|
174
216
|
*/
|
|
175
217
|
export declare function sanitizeNamespace(namespace?: string): Result<string>;
|
|
176
218
|
|
|
219
|
+
/**
|
|
220
|
+
* How a file's bytes are represented in the `contents` field on the wire.
|
|
221
|
+
*
|
|
222
|
+
* @remarks
|
|
223
|
+
* - `'utf8'` — `contents` is the file decoded as UTF-8 text. This is the
|
|
224
|
+
* historical (and default) representation, and it is **lossy**: the decode is
|
|
225
|
+
* the lenient WHATWG one, so any byte sequence that is not valid UTF-8 has
|
|
226
|
+
* already been replaced by U+FFFD by the time it reaches the wire, and the
|
|
227
|
+
* substitution is not recoverable downstream.
|
|
228
|
+
* - `'base64'` — `contents` is the file's bytes, base64-encoded. Byte-faithful
|
|
229
|
+
* and therefore the only representation over which a consumer can decide
|
|
230
|
+
* whether the stored bytes were valid UTF-8, or read genuinely binary content.
|
|
231
|
+
* Costs roughly 33% more payload.
|
|
232
|
+
* @public
|
|
233
|
+
*/
|
|
234
|
+
export declare type StorageContentEncoding = 'utf8' | 'base64';
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Converter for the wire content encoding.
|
|
238
|
+
* @public
|
|
239
|
+
*/
|
|
240
|
+
export declare const storageContentEncoding: Converter<StorageContentEncoding>;
|
|
241
|
+
|
|
177
242
|
/**
|
|
178
243
|
* Converter for file responses.
|
|
179
244
|
* @public
|
|
@@ -198,6 +263,12 @@ export declare type StorageNamespace = string;
|
|
|
198
263
|
*/
|
|
199
264
|
export declare const storagePathRequest: Converter<IStoragePathRequest>;
|
|
200
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Converter for read-file requests.
|
|
268
|
+
* @public
|
|
269
|
+
*/
|
|
270
|
+
export declare const storageReadFileRequest: Converter<IStorageReadFileRequest>;
|
|
271
|
+
|
|
201
272
|
/**
|
|
202
273
|
* Converter for sync requests.
|
|
203
274
|
* @public
|
package/dist/tsdoc-metadata.json
CHANGED
package/eslint.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ESLint 9 flat config
|
|
2
|
+
const nodeProfile = require('@rushstack/eslint-config/flat/profile/node');
|
|
3
|
+
const packletsPlugin = require('@rushstack/eslint-config/flat/mixins/packlets');
|
|
4
|
+
const tsdocPlugin = require('@rushstack/eslint-config/flat/mixins/tsdoc');
|
|
5
|
+
|
|
6
|
+
module.exports = [
|
|
7
|
+
...nodeProfile,
|
|
8
|
+
packletsPlugin,
|
|
9
|
+
...tsdocPlugin,
|
|
10
|
+
{
|
|
11
|
+
// Override specific rules if needed
|
|
12
|
+
rules: {
|
|
13
|
+
'@rushstack/packlets/mechanics': 'warn'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Converter } from '@fgv/ts-utils';
|
|
2
|
-
import type { IStorageFileResponse, IStoragePathRequest, IStorageSyncRequest, IStorageTreeChildrenResponse, IStorageTreeItem, IStorageWriteFileRequest } from './model';
|
|
2
|
+
import type { IStorageFileResponse, IStoragePathRequest, IStorageReadFileRequest, IStorageSyncRequest, IStorageTreeChildrenResponse, IStorageTreeItem, IStorageWriteFileRequest, StorageContentEncoding } from './model';
|
|
3
3
|
/**
|
|
4
4
|
* Converter for {@link IStorageTreeItem}.
|
|
5
5
|
* @public
|
|
@@ -10,6 +10,16 @@ export declare const storageTreeItem: Converter<IStorageTreeItem>;
|
|
|
10
10
|
* @public
|
|
11
11
|
*/
|
|
12
12
|
export declare const storagePathRequest: Converter<IStoragePathRequest>;
|
|
13
|
+
/**
|
|
14
|
+
* Converter for the wire content encoding.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export declare const storageContentEncoding: Converter<StorageContentEncoding>;
|
|
18
|
+
/**
|
|
19
|
+
* Converter for read-file requests.
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export declare const storageReadFileRequest: Converter<IStorageReadFileRequest>;
|
|
13
23
|
/**
|
|
14
24
|
* Converter for write-file requests.
|
|
15
25
|
* @public
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"converters.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/converters.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,KAAK,SAAS,EAAc,MAAM,eAAe,CAAC;AAE3D,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,
|
|
1
|
+
{"version":3,"file":"converters.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/converters.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,KAAK,SAAS,EAAc,MAAM,eAAe,CAAC;AAE3D,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,EAEvB,MAAM,SAAS,CAAC;AAOjB;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,CAAC,gBAAgB,CAItD,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,mBAAmB,CAI1D,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,CAAC,sBAAsB,CACG,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,CAAC,uBAAuB,CAKlE,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,CAAC,wBAAwB,CAMpE,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,mBAAmB,CAG1D,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,CAAC,oBAAoB,CAM5D,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,SAAS,CAAC,4BAA4B,CAI5E,CAAC"}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.storageTreeChildrenResponse = exports.storageFileResponse = exports.storageSyncRequest = exports.storageWriteFileRequest = exports.storagePathRequest = exports.storageTreeItem = void 0;
|
|
24
|
+
exports.storageTreeChildrenResponse = exports.storageFileResponse = exports.storageSyncRequest = exports.storageWriteFileRequest = exports.storageReadFileRequest = exports.storageContentEncoding = exports.storagePathRequest = exports.storageTreeItem = void 0;
|
|
25
25
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
26
26
|
const storageItemType = ts_utils_1.Converters.enumeratedValue([
|
|
27
27
|
'file',
|
|
@@ -44,6 +44,20 @@ exports.storagePathRequest = ts_utils_1.Converters.strictObject({
|
|
|
44
44
|
path: ts_utils_1.Converters.string,
|
|
45
45
|
namespace: ts_utils_1.Converters.string.optional()
|
|
46
46
|
});
|
|
47
|
+
/**
|
|
48
|
+
* Converter for the wire content encoding.
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
exports.storageContentEncoding = ts_utils_1.Converters.enumeratedValue(['utf8', 'base64']);
|
|
52
|
+
/**
|
|
53
|
+
* Converter for read-file requests.
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
exports.storageReadFileRequest = ts_utils_1.Converters.strictObject({
|
|
57
|
+
path: ts_utils_1.Converters.string,
|
|
58
|
+
namespace: ts_utils_1.Converters.string.optional(),
|
|
59
|
+
encoding: exports.storageContentEncoding.optional()
|
|
60
|
+
});
|
|
47
61
|
/**
|
|
48
62
|
* Converter for write-file requests.
|
|
49
63
|
* @public
|
|
@@ -68,7 +82,8 @@ exports.storageSyncRequest = ts_utils_1.Converters.strictObject({
|
|
|
68
82
|
exports.storageFileResponse = ts_utils_1.Converters.strictObject({
|
|
69
83
|
path: ts_utils_1.Converters.string,
|
|
70
84
|
contents: ts_utils_1.Converters.string,
|
|
71
|
-
contentType: ts_utils_1.Converters.string.optional()
|
|
85
|
+
contentType: ts_utils_1.Converters.string.optional(),
|
|
86
|
+
encoding: exports.storageContentEncoding.optional()
|
|
72
87
|
});
|
|
73
88
|
/**
|
|
74
89
|
* Converter for children list responses.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"converters.js","sourceRoot":"","sources":["../../../src/packlets/storage/converters.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAEH,4CAA2D;
|
|
1
|
+
{"version":3,"file":"converters.js","sourceRoot":"","sources":["../../../src/packlets/storage/converters.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAEH,4CAA2D;AAc3D,MAAM,eAAe,GAA+B,qBAAU,CAAC,eAAe,CAAkB;IAC9F,MAAM;IACN,WAAW;CACZ,CAAC,CAAC;AAEH;;;GAGG;AACU,QAAA,eAAe,GAAgC,qBAAU,CAAC,YAAY,CAAmB;IACpG,IAAI,EAAE,qBAAU,CAAC,MAAM;IACvB,IAAI,EAAE,qBAAU,CAAC,MAAM;IACvB,IAAI,EAAE,eAAe;CACtB,CAAC,CAAC;AAEH;;;GAGG;AACU,QAAA,kBAAkB,GAC7B,qBAAU,CAAC,YAAY,CAAsB;IAC3C,IAAI,EAAE,qBAAU,CAAC,MAAM;IACvB,SAAS,EAAE,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAEL;;;GAGG;AACU,QAAA,sBAAsB,GACjC,qBAAU,CAAC,eAAe,CAAyB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEzE;;;GAGG;AACU,QAAA,sBAAsB,GACjC,qBAAU,CAAC,YAAY,CAA0B;IAC/C,IAAI,EAAE,qBAAU,CAAC,MAAM;IACvB,SAAS,EAAE,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE;IACvC,QAAQ,EAAE,8BAAsB,CAAC,QAAQ,EAAE;CAC5C,CAAC,CAAC;AAEL;;;GAGG;AACU,QAAA,uBAAuB,GAClC,qBAAU,CAAC,YAAY,CAA2B;IAChD,IAAI,EAAE,qBAAU,CAAC,MAAM;IACvB,QAAQ,EAAE,qBAAU,CAAC,MAAM;IAC3B,WAAW,EAAE,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE;IACzC,SAAS,EAAE,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAEL;;;GAGG;AACU,QAAA,kBAAkB,GAC7B,qBAAU,CAAC,YAAY,CAAsB;IAC3C,SAAS,EAAE,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAEL;;;GAGG;AACU,QAAA,mBAAmB,GAC9B,qBAAU,CAAC,YAAY,CAAuB;IAC5C,IAAI,EAAE,qBAAU,CAAC,MAAM;IACvB,QAAQ,EAAE,qBAAU,CAAC,MAAM;IAC3B,WAAW,EAAE,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE;IACzC,QAAQ,EAAE,8BAAsB,CAAC,QAAQ,EAAE;CAC5C,CAAC,CAAC;AAEL;;;GAGG;AACU,QAAA,2BAA2B,GACtC,qBAAU,CAAC,YAAY,CAA+B;IACpD,IAAI,EAAE,qBAAU,CAAC,MAAM;IACvB,QAAQ,EAAE,qBAAU,CAAC,OAAO,CAAC,uBAAe,CAAC;CAC9C,CAAC,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport { type Converter, Converters } from '@fgv/ts-utils';\n\nimport type {\n IStorageFileResponse,\n IStoragePathRequest,\n IStorageReadFileRequest,\n IStorageSyncRequest,\n IStorageTreeChildrenResponse,\n IStorageTreeItem,\n IStorageWriteFileRequest,\n StorageContentEncoding,\n StorageItemType\n} from './model';\n\nconst storageItemType: Converter<StorageItemType> = Converters.enumeratedValue<StorageItemType>([\n 'file',\n 'directory'\n]);\n\n/**\n * Converter for {@link IStorageTreeItem}.\n * @public\n */\nexport const storageTreeItem: Converter<IStorageTreeItem> = Converters.strictObject<IStorageTreeItem>({\n path: Converters.string,\n name: Converters.string,\n type: storageItemType\n});\n\n/**\n * Converter for path-based requests.\n * @public\n */\nexport const storagePathRequest: Converter<IStoragePathRequest> =\n Converters.strictObject<IStoragePathRequest>({\n path: Converters.string,\n namespace: Converters.string.optional()\n });\n\n/**\n * Converter for the wire content encoding.\n * @public\n */\nexport const storageContentEncoding: Converter<StorageContentEncoding> =\n Converters.enumeratedValue<StorageContentEncoding>(['utf8', 'base64']);\n\n/**\n * Converter for read-file requests.\n * @public\n */\nexport const storageReadFileRequest: Converter<IStorageReadFileRequest> =\n Converters.strictObject<IStorageReadFileRequest>({\n path: Converters.string,\n namespace: Converters.string.optional(),\n encoding: storageContentEncoding.optional()\n });\n\n/**\n * Converter for write-file requests.\n * @public\n */\nexport const storageWriteFileRequest: Converter<IStorageWriteFileRequest> =\n Converters.strictObject<IStorageWriteFileRequest>({\n path: Converters.string,\n contents: Converters.string,\n contentType: Converters.string.optional(),\n namespace: Converters.string.optional()\n });\n\n/**\n * Converter for sync requests.\n * @public\n */\nexport const storageSyncRequest: Converter<IStorageSyncRequest> =\n Converters.strictObject<IStorageSyncRequest>({\n namespace: Converters.string.optional()\n });\n\n/**\n * Converter for file responses.\n * @public\n */\nexport const storageFileResponse: Converter<IStorageFileResponse> =\n Converters.strictObject<IStorageFileResponse>({\n path: Converters.string,\n contents: Converters.string,\n contentType: Converters.string.optional(),\n encoding: storageContentEncoding.optional()\n });\n\n/**\n * Converter for children list responses.\n * @public\n */\nexport const storageTreeChildrenResponse: Converter<IStorageTreeChildrenResponse> =\n Converters.strictObject<IStorageTreeChildrenResponse>({\n path: Converters.string,\n children: Converters.arrayOf(storageTreeItem)\n });\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Result } from '@fgv/ts-utils';
|
|
2
|
-
import type { IHttpStorageProvider, IStorageFileResponse, IStorageSyncResponse, IStorageTreeItem } from './model';
|
|
2
|
+
import type { IHttpStorageProvider, IStorageFileResponse, IStorageSyncResponse, IStorageTreeItem, StorageContentEncoding } from './model';
|
|
3
3
|
import type { IHttpStorageProviderFactory } from './provider';
|
|
4
4
|
/**
|
|
5
5
|
* Options for creating filesystem-backed storage providers.
|
|
@@ -17,7 +17,7 @@ export declare class FsStorageProvider implements IHttpStorageProvider {
|
|
|
17
17
|
constructor(rootPath: string);
|
|
18
18
|
getItem(itemPath: string): Promise<Result<IStorageTreeItem>>;
|
|
19
19
|
getChildren(itemPath: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>>;
|
|
20
|
-
getFile(itemPath: string): Promise<Result<IStorageFileResponse>>;
|
|
20
|
+
getFile(itemPath: string, encoding?: StorageContentEncoding): Promise<Result<IStorageFileResponse>>;
|
|
21
21
|
saveFile(itemPath: string, contents: string, contentType?: string): Promise<Result<IStorageFileResponse>>;
|
|
22
22
|
deleteFile(itemPath: string): Promise<Result<boolean>>;
|
|
23
23
|
createDirectory(itemPath: string): Promise<Result<IStorageTreeItem>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fsProvider.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/fsProvider.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAuB,KAAK,MAAM,EAAW,MAAM,eAAe,CAAC;AAE1E,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"fsProvider.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/fsProvider.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAuB,KAAK,MAAM,EAAW,MAAM,eAAe,CAAC;AAE1E,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,sBAAsB,EAEvB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAMD;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,oBAAoB;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEhB,QAAQ,EAAE,MAAM;IAItB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAa5D,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAa/E,OAAO,CAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,sBAAsB,GAChC,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAoC3B,QAAQ,CACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAmB3B,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAiBtD,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAapE,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAI1D,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,WAAW;CASpB;AAuBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;;GAGG;AACH,qBAAa,wBAAyB,YAAW,2BAA2B;IAC1E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEhB,OAAO,EAAE,gCAAgC;IAIrD,YAAY,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,oBAAoB,CAAC;CAmBtE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CASpE"}
|
|
@@ -98,7 +98,7 @@ class FsStorageProvider {
|
|
|
98
98
|
return (0, ts_utils_1.fail)(`${itemPath}: ${_toMessage(err)}`);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
async getFile(itemPath) {
|
|
101
|
+
async getFile(itemPath, encoding) {
|
|
102
102
|
const resolved = this._resolveAbsolutePath(itemPath);
|
|
103
103
|
if (resolved.isFailure()) {
|
|
104
104
|
return (0, ts_utils_1.fail)(resolved.message);
|
|
@@ -108,6 +108,22 @@ class FsStorageProvider {
|
|
|
108
108
|
if (!stats.isFile()) {
|
|
109
109
|
return (0, ts_utils_1.fail)(`${itemPath}: not a file`);
|
|
110
110
|
}
|
|
111
|
+
// Byte-faithful only when asked. On the base64 branch `readFile` is called
|
|
112
|
+
// with no encoding argument, so it yields the raw bytes and base64 carries
|
|
113
|
+
// them intact. The default branch below still decodes as UTF-8 — lossy, but
|
|
114
|
+
// it is what every existing caller already receives.
|
|
115
|
+
if (encoding === 'base64') {
|
|
116
|
+
const bytes = await fsp.readFile(resolved.value);
|
|
117
|
+
return (0, ts_utils_1.succeed)({
|
|
118
|
+
path: normalizeRequestPath(itemPath),
|
|
119
|
+
contents: bytes.toString('base64'),
|
|
120
|
+
encoding: 'base64'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Deliberately no `encoding` field on the default path: absent means `'utf8'`,
|
|
124
|
+
// so an existing deployment's responses stay byte-identical. It also leaves a
|
|
125
|
+
// usable signal — a client that asked for base64 and got no field back knows
|
|
126
|
+
// the request was not honoured, rather than having to guess.
|
|
111
127
|
const contents = await fsp.readFile(resolved.value, 'utf8');
|
|
112
128
|
return (0, ts_utils_1.succeed)({
|
|
113
129
|
path: normalizeRequestPath(itemPath),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fsProvider.js","sourceRoot":"","sources":["../../../src/packlets/storage/fsProvider.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as fs from 'fs';\nimport * as fsp from 'fs/promises';\nimport * as path from 'path';\n\nimport { captureResult, fail, type Result, succeed } from '@fgv/ts-utils';\n\nimport type {\n IHttpStorageProvider,\n IStorageFileResponse,\n IStorageSyncResponse,\n IStorageTreeItem,\n StorageItemType\n} from './model';\nimport type { IHttpStorageProviderFactory } from './provider';\n\n/**\n * Options for creating filesystem-backed storage providers.\n * @public\n */\nexport interface IFsStorageProviderFactoryOptions {\n readonly rootPath: string;\n}\n\nfunction toItemType(stats: fs.Stats): StorageItemType {\n return stats.isDirectory() ? 'directory' : 'file';\n}\n\n/**\n * Filesystem-backed implementation of {@link IHttpStorageProvider}.\n * @public\n */\nexport class FsStorageProvider implements IHttpStorageProvider {\n private readonly _rootPath: string;\n\n public constructor(rootPath: string) {\n this._rootPath = rootPath;\n }\n\n public async getItem(itemPath: string): Promise<Result<IStorageTreeItem>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const stats = await fsp.stat(resolved.value);\n return succeed(this._toTreeItem(itemPath, toItemType(stats)));\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async getChildren(itemPath: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const entries = await fsp.readdir(resolved.value, { withFileTypes: true });\n return _mapEntries(entries, itemPath);\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async getFile(itemPath: string): Promise<Result<IStorageFileResponse>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const stats = await fsp.stat(resolved.value);\n if (!stats.isFile()) {\n return fail(`${itemPath}: not a file`);\n }\n const contents = await fsp.readFile(resolved.value, 'utf8');\n return succeed({\n path: normalizeRequestPath(itemPath),\n contents\n });\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async saveFile(\n itemPath: string,\n contents: string,\n contentType?: string\n ): Promise<Result<IStorageFileResponse>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const parentDir = path.dirname(resolved.value);\n await fsp.mkdir(parentDir, { recursive: true });\n await fsp.writeFile(resolved.value, contents, 'utf8');\n return succeed({\n path: normalizeRequestPath(itemPath),\n contents,\n contentType\n });\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async deleteFile(itemPath: string): Promise<Result<boolean>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const stats = await fsp.stat(resolved.value);\n if (!stats.isFile()) {\n return fail(`${itemPath}: not a file`);\n }\n await fsp.unlink(resolved.value);\n return succeed(true);\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async createDirectory(itemPath: string): Promise<Result<IStorageTreeItem>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n await fsp.mkdir(resolved.value, { recursive: true });\n return succeed(this._toTreeItem(itemPath, 'directory'));\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async sync(): Promise<Result<IStorageSyncResponse>> {\n return succeed({ synced: 0 });\n }\n\n private _resolveAbsolutePath(requestPath: string): Result<string> {\n const normalized = normalizeRequestPath(requestPath);\n const candidate = path.resolve(this._rootPath, `.${normalized}`);\n const relative = path.relative(this._rootPath, candidate);\n if (relative.startsWith('..') || path.isAbsolute(relative)) {\n return fail(`${requestPath}: path is outside storage root`);\n }\n return succeed(candidate);\n }\n\n private _toTreeItem(itemPath: string, type: StorageItemType): IStorageTreeItem {\n const normalizedPath = normalizeRequestPath(itemPath);\n const name = normalizedPath === '/' ? '/' : path.posix.basename(normalizedPath);\n return {\n path: normalizedPath,\n name,\n type\n };\n }\n}\n\nfunction _mapEntries(\n entries: ReadonlyArray<fs.Dirent>,\n parentPath: string\n): Result<ReadonlyArray<IStorageTreeItem>> {\n const normalizedParent = normalizeRequestPath(parentPath);\n const items: IStorageTreeItem[] = entries.map((entry) => {\n const isDirectory = entry.isDirectory();\n const childPath = normalizedParent === '/' ? `/${entry.name}` : `${normalizedParent}/${entry.name}`;\n return {\n path: childPath,\n name: entry.name,\n type: isDirectory ? 'directory' : 'file'\n };\n });\n return succeed(items);\n}\n\nfunction _toMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Normalizes a request path to a consistent POSIX format.\n * @public\n */\nexport function normalizeRequestPath(requestPath: string): string {\n if (requestPath.length === 0) {\n return '/';\n }\n const normalized = path.posix.normalize(requestPath.startsWith('/') ? requestPath : `/${requestPath}`);\n return normalized.startsWith('/') ? normalized : `/${normalized}`;\n}\n\n/**\n * Namespace-aware provider factory backed by filesystem directories.\n * @public\n */\nexport class FsStorageProviderFactory implements IHttpStorageProviderFactory {\n private readonly _rootPath: string;\n\n public constructor(options: IFsStorageProviderFactoryOptions) {\n this._rootPath = path.resolve(options.rootPath);\n }\n\n public forNamespace(namespace?: string): Result<IHttpStorageProvider> {\n const safeNamespace = sanitizeNamespace(namespace);\n if (safeNamespace.isFailure()) {\n return fail(safeNamespace.message);\n }\n\n const namespaceRoot = path.resolve(this._rootPath, safeNamespace.value);\n const relative = path.relative(this._rootPath, namespaceRoot);\n if (relative.startsWith('..') || path.isAbsolute(relative)) {\n return fail(`namespace '${safeNamespace.value}' is outside root`);\n }\n\n const ensureResult = captureResult(() => fs.mkdirSync(namespaceRoot, { recursive: true }));\n if (ensureResult.isFailure()) {\n return fail(`namespace '${safeNamespace.value}': ${ensureResult.message}`);\n }\n\n return succeed(new FsStorageProvider(namespaceRoot));\n }\n}\n\n/**\n * Sanitize namespace path segment.\n * @public\n */\nexport function sanitizeNamespace(namespace?: string): Result<string> {\n if (!namespace || namespace.trim().length === 0) {\n return succeed('default');\n }\n const trimmed = namespace.trim();\n if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) {\n return fail(`namespace '${namespace}': only [a-zA-Z0-9._-] allowed`);\n }\n return succeed(trimmed);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fsProvider.js","sourceRoot":"","sources":["../../../src/packlets/storage/fsProvider.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as fs from 'fs';\nimport * as fsp from 'fs/promises';\nimport * as path from 'path';\n\nimport { captureResult, fail, type Result, succeed } from '@fgv/ts-utils';\n\nimport type {\n IHttpStorageProvider,\n IStorageFileResponse,\n IStorageSyncResponse,\n IStorageTreeItem,\n StorageContentEncoding,\n StorageItemType\n} from './model';\nimport type { IHttpStorageProviderFactory } from './provider';\n\n/**\n * Options for creating filesystem-backed storage providers.\n * @public\n */\nexport interface IFsStorageProviderFactoryOptions {\n readonly rootPath: string;\n}\n\nfunction toItemType(stats: fs.Stats): StorageItemType {\n return stats.isDirectory() ? 'directory' : 'file';\n}\n\n/**\n * Filesystem-backed implementation of {@link IHttpStorageProvider}.\n * @public\n */\nexport class FsStorageProvider implements IHttpStorageProvider {\n private readonly _rootPath: string;\n\n public constructor(rootPath: string) {\n this._rootPath = rootPath;\n }\n\n public async getItem(itemPath: string): Promise<Result<IStorageTreeItem>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const stats = await fsp.stat(resolved.value);\n return succeed(this._toTreeItem(itemPath, toItemType(stats)));\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async getChildren(itemPath: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const entries = await fsp.readdir(resolved.value, { withFileTypes: true });\n return _mapEntries(entries, itemPath);\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async getFile(\n itemPath: string,\n encoding?: StorageContentEncoding\n ): Promise<Result<IStorageFileResponse>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const stats = await fsp.stat(resolved.value);\n if (!stats.isFile()) {\n return fail(`${itemPath}: not a file`);\n }\n // Byte-faithful only when asked. On the base64 branch `readFile` is called\n // with no encoding argument, so it yields the raw bytes and base64 carries\n // them intact. The default branch below still decodes as UTF-8 — lossy, but\n // it is what every existing caller already receives.\n if (encoding === 'base64') {\n const bytes = await fsp.readFile(resolved.value);\n return succeed({\n path: normalizeRequestPath(itemPath),\n contents: bytes.toString('base64'),\n encoding: 'base64'\n });\n }\n // Deliberately no `encoding` field on the default path: absent means `'utf8'`,\n // so an existing deployment's responses stay byte-identical. It also leaves a\n // usable signal — a client that asked for base64 and got no field back knows\n // the request was not honoured, rather than having to guess.\n const contents = await fsp.readFile(resolved.value, 'utf8');\n return succeed({\n path: normalizeRequestPath(itemPath),\n contents\n });\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async saveFile(\n itemPath: string,\n contents: string,\n contentType?: string\n ): Promise<Result<IStorageFileResponse>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const parentDir = path.dirname(resolved.value);\n await fsp.mkdir(parentDir, { recursive: true });\n await fsp.writeFile(resolved.value, contents, 'utf8');\n return succeed({\n path: normalizeRequestPath(itemPath),\n contents,\n contentType\n });\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async deleteFile(itemPath: string): Promise<Result<boolean>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n const stats = await fsp.stat(resolved.value);\n if (!stats.isFile()) {\n return fail(`${itemPath}: not a file`);\n }\n await fsp.unlink(resolved.value);\n return succeed(true);\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async createDirectory(itemPath: string): Promise<Result<IStorageTreeItem>> {\n const resolved = this._resolveAbsolutePath(itemPath);\n if (resolved.isFailure()) {\n return fail(resolved.message);\n }\n try {\n await fsp.mkdir(resolved.value, { recursive: true });\n return succeed(this._toTreeItem(itemPath, 'directory'));\n } catch (err: unknown) {\n return fail(`${itemPath}: ${_toMessage(err)}`);\n }\n }\n\n public async sync(): Promise<Result<IStorageSyncResponse>> {\n return succeed({ synced: 0 });\n }\n\n private _resolveAbsolutePath(requestPath: string): Result<string> {\n const normalized = normalizeRequestPath(requestPath);\n const candidate = path.resolve(this._rootPath, `.${normalized}`);\n const relative = path.relative(this._rootPath, candidate);\n if (relative.startsWith('..') || path.isAbsolute(relative)) {\n return fail(`${requestPath}: path is outside storage root`);\n }\n return succeed(candidate);\n }\n\n private _toTreeItem(itemPath: string, type: StorageItemType): IStorageTreeItem {\n const normalizedPath = normalizeRequestPath(itemPath);\n const name = normalizedPath === '/' ? '/' : path.posix.basename(normalizedPath);\n return {\n path: normalizedPath,\n name,\n type\n };\n }\n}\n\nfunction _mapEntries(\n entries: ReadonlyArray<fs.Dirent>,\n parentPath: string\n): Result<ReadonlyArray<IStorageTreeItem>> {\n const normalizedParent = normalizeRequestPath(parentPath);\n const items: IStorageTreeItem[] = entries.map((entry) => {\n const isDirectory = entry.isDirectory();\n const childPath = normalizedParent === '/' ? `/${entry.name}` : `${normalizedParent}/${entry.name}`;\n return {\n path: childPath,\n name: entry.name,\n type: isDirectory ? 'directory' : 'file'\n };\n });\n return succeed(items);\n}\n\nfunction _toMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Normalizes a request path to a consistent POSIX format.\n * @public\n */\nexport function normalizeRequestPath(requestPath: string): string {\n if (requestPath.length === 0) {\n return '/';\n }\n const normalized = path.posix.normalize(requestPath.startsWith('/') ? requestPath : `/${requestPath}`);\n return normalized.startsWith('/') ? normalized : `/${normalized}`;\n}\n\n/**\n * Namespace-aware provider factory backed by filesystem directories.\n * @public\n */\nexport class FsStorageProviderFactory implements IHttpStorageProviderFactory {\n private readonly _rootPath: string;\n\n public constructor(options: IFsStorageProviderFactoryOptions) {\n this._rootPath = path.resolve(options.rootPath);\n }\n\n public forNamespace(namespace?: string): Result<IHttpStorageProvider> {\n const safeNamespace = sanitizeNamespace(namespace);\n if (safeNamespace.isFailure()) {\n return fail(safeNamespace.message);\n }\n\n const namespaceRoot = path.resolve(this._rootPath, safeNamespace.value);\n const relative = path.relative(this._rootPath, namespaceRoot);\n if (relative.startsWith('..') || path.isAbsolute(relative)) {\n return fail(`namespace '${safeNamespace.value}' is outside root`);\n }\n\n const ensureResult = captureResult(() => fs.mkdirSync(namespaceRoot, { recursive: true }));\n if (ensureResult.isFailure()) {\n return fail(`namespace '${safeNamespace.value}': ${ensureResult.message}`);\n }\n\n return succeed(new FsStorageProvider(namespaceRoot));\n }\n}\n\n/**\n * Sanitize namespace path segment.\n * @public\n */\nexport function sanitizeNamespace(namespace?: string): Result<string> {\n if (!namespace || namespace.trim().length === 0) {\n return succeed('default');\n }\n const trimmed = namespace.trim();\n if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) {\n return fail(`namespace '${namespace}': only [a-zA-Z0-9._-] allowed`);\n }\n return succeed(trimmed);\n}\n"]}
|
|
@@ -9,6 +9,22 @@ export type StorageNamespace = string;
|
|
|
9
9
|
* @public
|
|
10
10
|
*/
|
|
11
11
|
export type StorageItemType = 'file' | 'directory';
|
|
12
|
+
/**
|
|
13
|
+
* How a file's bytes are represented in the `contents` field on the wire.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* - `'utf8'` — `contents` is the file decoded as UTF-8 text. This is the
|
|
17
|
+
* historical (and default) representation, and it is **lossy**: the decode is
|
|
18
|
+
* the lenient WHATWG one, so any byte sequence that is not valid UTF-8 has
|
|
19
|
+
* already been replaced by U+FFFD by the time it reaches the wire, and the
|
|
20
|
+
* substitution is not recoverable downstream.
|
|
21
|
+
* - `'base64'` — `contents` is the file's bytes, base64-encoded. Byte-faithful
|
|
22
|
+
* and therefore the only representation over which a consumer can decide
|
|
23
|
+
* whether the stored bytes were valid UTF-8, or read genuinely binary content.
|
|
24
|
+
* Costs roughly 33% more payload.
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export type StorageContentEncoding = 'utf8' | 'base64';
|
|
12
28
|
/**
|
|
13
29
|
* Storage tree item metadata.
|
|
14
30
|
* @public
|
|
@@ -34,6 +50,19 @@ export interface IStorageFileResponse {
|
|
|
34
50
|
readonly path: string;
|
|
35
51
|
readonly contents: string;
|
|
36
52
|
readonly contentType?: string;
|
|
53
|
+
/**
|
|
54
|
+
* How `contents` is encoded. Absent means `'utf8'`.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* **This field describes what the server actually produced, not what the
|
|
58
|
+
* client asked for**, and a client must branch on it rather than on its own
|
|
59
|
+
* request. A provider that does not implement `'base64'` ignores the request
|
|
60
|
+
* and answers with UTF-8 text; a client that base64-decoded on the strength of
|
|
61
|
+
* having *asked* would corrupt every such response. Trusting the field instead
|
|
62
|
+
* makes an older or simpler provider degrade to today's behavior rather than
|
|
63
|
+
* to garbage.
|
|
64
|
+
*/
|
|
65
|
+
readonly encoding?: StorageContentEncoding;
|
|
37
66
|
}
|
|
38
67
|
/**
|
|
39
68
|
* Request for path-based operations.
|
|
@@ -43,6 +72,26 @@ export interface IStoragePathRequest {
|
|
|
43
72
|
readonly path: string;
|
|
44
73
|
readonly namespace?: StorageNamespace;
|
|
45
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Request for reading a file, with an optional preferred content encoding.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* `encoding` is a *preference*, not a demand: a provider that cannot honour it
|
|
80
|
+
* answers in `'utf8'` and says so on {@link IStorageFileResponse.encoding}. The
|
|
81
|
+
* response is the authority.
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
export interface IStorageReadFileRequest extends IStoragePathRequest {
|
|
85
|
+
/**
|
|
86
|
+
* Preferred wire encoding for the response's `contents`. Absent means `'utf8'`.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* A preference, not a demand — see {@link IStorageFileResponse.encoding}, which
|
|
90
|
+
* reports what the server actually produced and is the field a client must
|
|
91
|
+
* branch on.
|
|
92
|
+
*/
|
|
93
|
+
readonly encoding?: StorageContentEncoding;
|
|
94
|
+
}
|
|
46
95
|
/**
|
|
47
96
|
* Request for writing file contents.
|
|
48
97
|
* @public
|
|
@@ -73,7 +122,15 @@ export interface IStorageSyncResponse {
|
|
|
73
122
|
export interface IHttpStorageProvider {
|
|
74
123
|
getItem(path: string): Promise<Result<IStorageTreeItem>>;
|
|
75
124
|
getChildren(path: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>>;
|
|
76
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Reads a file.
|
|
127
|
+
* @param path - Path of the file to read.
|
|
128
|
+
* @param encoding - Preferred wire encoding for the returned `contents`. A
|
|
129
|
+
* provider that does not implement the requested encoding MUST answer in
|
|
130
|
+
* `'utf8'` and report that on the response, rather than failing — the field is
|
|
131
|
+
* a preference and the response is the authority.
|
|
132
|
+
*/
|
|
133
|
+
getFile(path: string, encoding?: StorageContentEncoding): Promise<Result<IStorageFileResponse>>;
|
|
77
134
|
saveFile(path: string, contents: string, contentType?: string): Promise<Result<IStorageFileResponse>>;
|
|
78
135
|
deleteFile(path: string): Promise<Result<boolean>>;
|
|
79
136
|
createDirectory(path: string): Promise<Result<IStorageTreeItem>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/model.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,CAAC;AAEnD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;CACpD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/model.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;CACpD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAE9B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CAC5C;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAClE;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CAC5C;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACzD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC5E;;;;;;;OAOG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC;IACtG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACjE,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC;CAC/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/packlets/storage/model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport type { Result } from '@fgv/ts-utils';\n\n/**\n * Namespace identifier for scoped storage.\n * @public\n */\nexport type StorageNamespace = string;\n\n/**\n * Storage item type.\n * @public\n */\nexport type StorageItemType = 'file' | 'directory';\n\n/**\n * Storage tree item metadata.\n * @public\n */\nexport interface IStorageTreeItem {\n readonly path: string;\n readonly name: string;\n readonly type: StorageItemType;\n}\n\n/**\n * Response for listing children.\n * @public\n */\nexport interface IStorageTreeChildrenResponse {\n readonly path: string;\n readonly children: ReadonlyArray<IStorageTreeItem>;\n}\n\n/**\n * Response for reading a file.\n * @public\n */\nexport interface IStorageFileResponse {\n readonly path: string;\n readonly contents: string;\n readonly contentType?: string;\n}\n\n/**\n * Request for path-based operations.\n * @public\n */\nexport interface IStoragePathRequest {\n readonly path: string;\n readonly namespace?: StorageNamespace;\n}\n\n/**\n * Request for writing file contents.\n * @public\n */\nexport interface IStorageWriteFileRequest extends IStoragePathRequest {\n readonly contents: string;\n readonly contentType?: string;\n}\n\n/**\n * Request for sync operation.\n * @public\n */\nexport interface IStorageSyncRequest {\n readonly namespace?: StorageNamespace;\n}\n\n/**\n * Sync response metadata.\n * @public\n */\nexport interface IStorageSyncResponse {\n readonly synced: number;\n}\n\n/**\n * Provider contract for storage backends.\n * All methods are async to support both filesystem and database backends.\n * @public\n */\nexport interface IHttpStorageProvider {\n getItem(path: string): Promise<Result<IStorageTreeItem>>;\n getChildren(path: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>>;\n getFile(path: string): Promise<Result<IStorageFileResponse>>;\n saveFile(path: string, contents: string, contentType?: string): Promise<Result<IStorageFileResponse>>;\n deleteFile(path: string): Promise<Result<boolean>>;\n createDirectory(path: string): Promise<Result<IStorageTreeItem>>;\n sync(): Promise<Result<IStorageSyncResponse>>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/packlets/storage/model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport type { Result } from '@fgv/ts-utils';\n\n/**\n * Namespace identifier for scoped storage.\n * @public\n */\nexport type StorageNamespace = string;\n\n/**\n * Storage item type.\n * @public\n */\nexport type StorageItemType = 'file' | 'directory';\n\n/**\n * How a file's bytes are represented in the `contents` field on the wire.\n *\n * @remarks\n * - `'utf8'` — `contents` is the file decoded as UTF-8 text. This is the\n * historical (and default) representation, and it is **lossy**: the decode is\n * the lenient WHATWG one, so any byte sequence that is not valid UTF-8 has\n * already been replaced by U+FFFD by the time it reaches the wire, and the\n * substitution is not recoverable downstream.\n * - `'base64'` — `contents` is the file's bytes, base64-encoded. Byte-faithful\n * and therefore the only representation over which a consumer can decide\n * whether the stored bytes were valid UTF-8, or read genuinely binary content.\n * Costs roughly 33% more payload.\n * @public\n */\nexport type StorageContentEncoding = 'utf8' | 'base64';\n\n/**\n * Storage tree item metadata.\n * @public\n */\nexport interface IStorageTreeItem {\n readonly path: string;\n readonly name: string;\n readonly type: StorageItemType;\n}\n\n/**\n * Response for listing children.\n * @public\n */\nexport interface IStorageTreeChildrenResponse {\n readonly path: string;\n readonly children: ReadonlyArray<IStorageTreeItem>;\n}\n\n/**\n * Response for reading a file.\n * @public\n */\nexport interface IStorageFileResponse {\n readonly path: string;\n readonly contents: string;\n readonly contentType?: string;\n\n /**\n * How `contents` is encoded. Absent means `'utf8'`.\n *\n * @remarks\n * **This field describes what the server actually produced, not what the\n * client asked for**, and a client must branch on it rather than on its own\n * request. A provider that does not implement `'base64'` ignores the request\n * and answers with UTF-8 text; a client that base64-decoded on the strength of\n * having *asked* would corrupt every such response. Trusting the field instead\n * makes an older or simpler provider degrade to today's behavior rather than\n * to garbage.\n */\n readonly encoding?: StorageContentEncoding;\n}\n\n/**\n * Request for path-based operations.\n * @public\n */\nexport interface IStoragePathRequest {\n readonly path: string;\n readonly namespace?: StorageNamespace;\n}\n\n/**\n * Request for reading a file, with an optional preferred content encoding.\n *\n * @remarks\n * `encoding` is a *preference*, not a demand: a provider that cannot honour it\n * answers in `'utf8'` and says so on {@link IStorageFileResponse.encoding}. The\n * response is the authority.\n * @public\n */\nexport interface IStorageReadFileRequest extends IStoragePathRequest {\n /**\n * Preferred wire encoding for the response's `contents`. Absent means `'utf8'`.\n *\n * @remarks\n * A preference, not a demand — see {@link IStorageFileResponse.encoding}, which\n * reports what the server actually produced and is the field a client must\n * branch on.\n */\n readonly encoding?: StorageContentEncoding;\n}\n\n/**\n * Request for writing file contents.\n * @public\n */\nexport interface IStorageWriteFileRequest extends IStoragePathRequest {\n readonly contents: string;\n readonly contentType?: string;\n}\n\n/**\n * Request for sync operation.\n * @public\n */\nexport interface IStorageSyncRequest {\n readonly namespace?: StorageNamespace;\n}\n\n/**\n * Sync response metadata.\n * @public\n */\nexport interface IStorageSyncResponse {\n readonly synced: number;\n}\n\n/**\n * Provider contract for storage backends.\n * All methods are async to support both filesystem and database backends.\n * @public\n */\nexport interface IHttpStorageProvider {\n getItem(path: string): Promise<Result<IStorageTreeItem>>;\n getChildren(path: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>>;\n /**\n * Reads a file.\n * @param path - Path of the file to read.\n * @param encoding - Preferred wire encoding for the returned `contents`. A\n * provider that does not implement the requested encoding MUST answer in\n * `'utf8'` and report that on the response, rather than failing — the field is\n * a preference and the response is the authority.\n */\n getFile(path: string, encoding?: StorageContentEncoding): Promise<Result<IStorageFileResponse>>;\n saveFile(path: string, contents: string, contentType?: string): Promise<Result<IStorageFileResponse>>;\n deleteFile(path: string): Promise<Result<boolean>>;\n createDirectory(path: string): Promise<Result<IStorageTreeItem>>;\n sync(): Promise<Result<IStorageSyncResponse>>;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/routes.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/packlets/storage/routes.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAa7C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,2BAA2B,CAAC;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;CACnC;AAaD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,2BAA2B,GAAG,IAAI,CA8J9E"}
|
|
@@ -80,7 +80,8 @@ function createStorageRoutes(options) {
|
|
|
80
80
|
return c.json(converted.value);
|
|
81
81
|
});
|
|
82
82
|
routes.get('/file', async (c) => {
|
|
83
|
-
const
|
|
83
|
+
const requestedEncoding = c.req.query('encoding');
|
|
84
|
+
const parsed = converters_1.storageReadFileRequest.convert(Object.assign(Object.assign({}, parsePathQuery(c.req.query('path'), c.req.query('namespace'))), (requestedEncoding !== undefined ? { encoding: requestedEncoding } : {})));
|
|
84
85
|
if (parsed.isFailure()) {
|
|
85
86
|
return c.json({ error: `invalid request: ${parsed.message}` }, 400);
|
|
86
87
|
}
|