@fgv/ts-http-storage 5.1.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/ts-http-storage.build.chunks.jsonl +9 -0
- package/.rush/temp/fbfcc9487d290993ba47f1d36e9383196e7b12ac.tar.log +61 -0
- package/.rush/temp/operation/build/all.log +9 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +647 -0
- package/config/api-extractor.json +36 -0
- package/config/rig.json +10 -0
- package/config/typedoc.json +7 -0
- package/dist/ts-http-storage.d.ts +225 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/docs/README.md +298 -0
- package/docs/classes/FsStorageProvider.createDirectory.md +20 -0
- package/docs/classes/FsStorageProvider.getChildren.md +20 -0
- package/docs/classes/FsStorageProvider.getFile.md +20 -0
- package/docs/classes/FsStorageProvider.getItem.md +20 -0
- package/docs/classes/FsStorageProvider.md +134 -0
- package/docs/classes/FsStorageProvider.saveFile.md +22 -0
- package/docs/classes/FsStorageProvider.sync.md +13 -0
- package/docs/classes/FsStorageProviderFactory.forNamespace.md +20 -0
- package/docs/classes/FsStorageProviderFactory.md +69 -0
- package/docs/classes/HttpStorageService.createDirectory.md +20 -0
- package/docs/classes/HttpStorageService.getChildren.md +20 -0
- package/docs/classes/HttpStorageService.getFile.md +20 -0
- package/docs/classes/HttpStorageService.getItem.md +20 -0
- package/docs/classes/HttpStorageService.md +132 -0
- package/docs/classes/HttpStorageService.saveFile.md +20 -0
- package/docs/classes/HttpStorageService.sync.md +20 -0
- package/docs/functions/createStorageRoutes.md +11 -0
- package/docs/functions/normalizeRequestPath.md +11 -0
- package/docs/functions/sanitizeNamespace.md +11 -0
- package/docs/interfaces/ICreateStorageRoutesOptions.logger.md +9 -0
- package/docs/interfaces/ICreateStorageRoutesOptions.md +61 -0
- package/docs/interfaces/ICreateStorageRoutesOptions.providers.md +9 -0
- package/docs/interfaces/IFsStorageProviderFactoryOptions.md +44 -0
- package/docs/interfaces/IFsStorageProviderFactoryOptions.rootPath.md +9 -0
- package/docs/interfaces/IHttpStorageProvider.createDirectory.md +20 -0
- package/docs/interfaces/IHttpStorageProvider.getChildren.md +20 -0
- package/docs/interfaces/IHttpStorageProvider.getFile.md +20 -0
- package/docs/interfaces/IHttpStorageProvider.getItem.md +20 -0
- package/docs/interfaces/IHttpStorageProvider.md +102 -0
- package/docs/interfaces/IHttpStorageProvider.saveFile.md +22 -0
- package/docs/interfaces/IHttpStorageProvider.sync.md +13 -0
- package/docs/interfaces/IHttpStorageProviderFactory.forNamespace.md +20 -0
- package/docs/interfaces/IHttpStorageProviderFactory.md +36 -0
- package/docs/interfaces/IStorageFileResponse.contentType.md +9 -0
- package/docs/interfaces/IStorageFileResponse.contents.md +9 -0
- package/docs/interfaces/IStorageFileResponse.md +78 -0
- package/docs/interfaces/IStorageFileResponse.path.md +9 -0
- package/docs/interfaces/IStoragePathRequest.md +61 -0
- package/docs/interfaces/IStoragePathRequest.namespace.md +9 -0
- package/docs/interfaces/IStoragePathRequest.path.md +9 -0
- package/docs/interfaces/IStorageSyncRequest.md +44 -0
- package/docs/interfaces/IStorageSyncRequest.namespace.md +9 -0
- package/docs/interfaces/IStorageSyncResponse.md +44 -0
- package/docs/interfaces/IStorageSyncResponse.synced.md +9 -0
- package/docs/interfaces/IStorageTreeChildrenResponse.children.md +9 -0
- package/docs/interfaces/IStorageTreeChildrenResponse.md +61 -0
- package/docs/interfaces/IStorageTreeChildrenResponse.path.md +9 -0
- package/docs/interfaces/IStorageTreeItem.md +78 -0
- package/docs/interfaces/IStorageTreeItem.name.md +9 -0
- package/docs/interfaces/IStorageTreeItem.path.md +9 -0
- package/docs/interfaces/IStorageTreeItem.type.md +9 -0
- package/docs/interfaces/IStorageWriteFileRequest.contentType.md +9 -0
- package/docs/interfaces/IStorageWriteFileRequest.contents.md +9 -0
- package/docs/interfaces/IStorageWriteFileRequest.md +97 -0
- package/docs/type-aliases/StorageItemType.md +11 -0
- package/docs/type-aliases/StorageNamespace.md +11 -0
- package/docs/variables/storageFileResponse.md +9 -0
- package/docs/variables/storagePathRequest.md +9 -0
- package/docs/variables/storageSyncRequest.md +9 -0
- package/docs/variables/storageTreeChildrenResponse.md +9 -0
- package/docs/variables/storageTreeItem.md +9 -0
- package/docs/variables/storageWriteFileRequest.md +9 -0
- package/etc/ts-http-storage.api.md +184 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +43 -0
- package/lib/index.js.map +1 -0
- package/lib/packlets/storage/converters.d.ts +33 -0
- package/lib/packlets/storage/converters.d.ts.map +1 -0
- package/lib/packlets/storage/converters.js +81 -0
- package/lib/packlets/storage/converters.js.map +1 -0
- package/lib/packlets/storage/fsProvider.d.ts +47 -0
- package/lib/packlets/storage/fsProvider.d.ts.map +1 -0
- package/lib/packlets/storage/fsProvider.js +260 -0
- package/lib/packlets/storage/fsProvider.js.map +1 -0
- package/lib/packlets/storage/index.d.ts +11 -0
- package/lib/packlets/storage/index.d.ts.map +1 -0
- package/lib/packlets/storage/index.js +48 -0
- package/lib/packlets/storage/index.js.map +1 -0
- package/lib/packlets/storage/model.d.ts +82 -0
- package/lib/packlets/storage/model.d.ts.map +1 -0
- package/lib/packlets/storage/model.js +24 -0
- package/lib/packlets/storage/model.js.map +1 -0
- package/lib/packlets/storage/provider.d.ts +10 -0
- package/lib/packlets/storage/provider.d.ts.map +1 -0
- package/lib/packlets/storage/provider.js +24 -0
- package/lib/packlets/storage/provider.js.map +1 -0
- package/lib/packlets/storage/routes.d.ts +17 -0
- package/lib/packlets/storage/routes.d.ts.map +1 -0
- package/lib/packlets/storage/routes.js +170 -0
- package/lib/packlets/storage/routes.js.map +1 -0
- package/lib/packlets/storage/service.d.ts +20 -0
- package/lib/packlets/storage/service.d.ts.map +1 -0
- package/lib/packlets/storage/service.js +95 -0
- package/lib/packlets/storage/service.js.map +1 -0
- package/lib/test/unit/storage/fsProvider.test.d.ts +2 -0
- package/lib/test/unit/storage/fsProvider.test.d.ts.map +1 -0
- package/lib/test/unit/storage/fsProvider.test.js +106 -0
- package/lib/test/unit/storage/fsProvider.test.js.map +1 -0
- package/lib/test/unit/storage/routes.test.d.ts +2 -0
- package/lib/test/unit/storage/routes.test.d.ts.map +1 -0
- package/lib/test/unit/storage/routes.test.js +124 -0
- package/lib/test/unit/storage/routes.test.js.map +1 -0
- package/lib/test/unit/storage/service.test.d.ts +2 -0
- package/lib/test/unit/storage/service.test.d.ts.map +1 -0
- package/lib/test/unit/storage/service.test.js +67 -0
- package/lib/test/unit/storage/service.test.js.map +1 -0
- package/package.json +66 -0
- package/rush-logs/ts-http-storage.build.cache.log +3 -0
- package/rush-logs/ts-http-storage.build.log +9 -0
- package/src/index.ts +28 -0
- package/src/packlets/storage/converters.ts +100 -0
- package/src/packlets/storage/fsProvider.ts +263 -0
- package/src/packlets/storage/index.ts +33 -0
- package/src/packlets/storage/model.ts +113 -0
- package/src/packlets/storage/provider.ts +33 -0
- package/src/packlets/storage/routes.ts +215 -0
- package/src/packlets/storage/service.ts +114 -0
- package/src/test/unit/storage/fsProvider.test.ts +129 -0
- package/src/test/unit/storage/routes.test.ts +165 -0
- package/src/test/unit/storage/service.test.ts +95 -0
- package/temp/build/typescript/ts_l9Fw4VUO.json +1 -0
- package/temp/ts-http-storage.api.json +3103 -0
- package/temp/ts-http-storage.api.md +184 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../../../src/test/unit/storage/service.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
4
|
+
const storage_1 = require("../../../packlets/storage");
|
|
5
|
+
class StubProvider {
|
|
6
|
+
async getItem(path) {
|
|
7
|
+
return (0, ts_utils_1.succeed)({ path, name: 'item', type: 'file' });
|
|
8
|
+
}
|
|
9
|
+
async getChildren(path) {
|
|
10
|
+
return (0, ts_utils_1.succeed)([{ path: `${path}/child.txt`, name: 'child.txt', type: 'file' }]);
|
|
11
|
+
}
|
|
12
|
+
async getFile(path) {
|
|
13
|
+
return (0, ts_utils_1.succeed)({ path, contents: 'contents' });
|
|
14
|
+
}
|
|
15
|
+
async saveFile(path, contents, contentType) {
|
|
16
|
+
return (0, ts_utils_1.succeed)({ path, contents, contentType });
|
|
17
|
+
}
|
|
18
|
+
async deleteFile(_path) {
|
|
19
|
+
return (0, ts_utils_1.succeed)(true);
|
|
20
|
+
}
|
|
21
|
+
async createDirectory(path) {
|
|
22
|
+
return (0, ts_utils_1.succeed)({ path, name: 'created', type: 'directory' });
|
|
23
|
+
}
|
|
24
|
+
async sync() {
|
|
25
|
+
return (0, ts_utils_1.succeed)({ synced: 7 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
class StubProviderFactory {
|
|
29
|
+
constructor(provider) {
|
|
30
|
+
this._provider = provider;
|
|
31
|
+
}
|
|
32
|
+
forNamespace() {
|
|
33
|
+
return (0, ts_utils_1.succeed)(this._provider);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class FailingProviderFactory {
|
|
37
|
+
forNamespace() {
|
|
38
|
+
return (0, ts_utils_1.fail)('missing provider');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
describe('HttpStorageService', () => {
|
|
42
|
+
test('maps getChildren response shape', async () => {
|
|
43
|
+
const service = new storage_1.HttpStorageService(new StubProviderFactory(new StubProvider()));
|
|
44
|
+
const result = await service.getChildren({ path: '/data', namespace: 'default' });
|
|
45
|
+
expect(result.isSuccess()).toBe(true);
|
|
46
|
+
expect(result.orThrow()).toEqual({
|
|
47
|
+
path: '/data',
|
|
48
|
+
children: [{ path: '/data/child.txt', name: 'child.txt', type: 'file' }]
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
test('formats provider lookup failures', async () => {
|
|
52
|
+
const service = new storage_1.HttpStorageService(new FailingProviderFactory());
|
|
53
|
+
const getResult = await service.getFile({ path: '/missing', namespace: 'default' });
|
|
54
|
+
expect(getResult.isFailure()).toBe(true);
|
|
55
|
+
expect(getResult.message).toBe('provider: missing provider');
|
|
56
|
+
const syncResult = await service.sync({ namespace: 'default' });
|
|
57
|
+
expect(syncResult.isFailure()).toBe(true);
|
|
58
|
+
expect(syncResult.message).toBe('provider: missing provider');
|
|
59
|
+
});
|
|
60
|
+
test('forwards deleteFile to provider', async () => {
|
|
61
|
+
const service = new storage_1.HttpStorageService(new StubProviderFactory(new StubProvider()));
|
|
62
|
+
const result = await service.deleteFile({ path: '/data/remove.txt', namespace: 'default' });
|
|
63
|
+
expect(result.isSuccess()).toBe(true);
|
|
64
|
+
expect(result.orThrow()).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=service.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.js","sourceRoot":"","sources":["../../../../src/test/unit/storage/service.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { fail, type Result, succeed } from '@fgv/ts-utils';\n\nimport {\n HttpStorageService,\n type IHttpStorageProvider,\n type IHttpStorageProviderFactory,\n type IStorageFileResponse,\n type IStorageSyncResponse,\n type IStorageTreeItem\n} from '../../../packlets/storage';\n\nclass StubProvider implements IHttpStorageProvider {\n public async getItem(path: string): Promise<Result<IStorageTreeItem>> {\n return succeed({ path, name: 'item', type: 'file' });\n }\n\n public async getChildren(path: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>> {\n return succeed([{ path: `${path}/child.txt`, name: 'child.txt', type: 'file' }]);\n }\n\n public async getFile(path: string): Promise<Result<IStorageFileResponse>> {\n return succeed({ path, contents: 'contents' });\n }\n\n public async saveFile(\n path: string,\n contents: string,\n contentType?: string\n ): Promise<Result<IStorageFileResponse>> {\n return succeed({ path, contents, contentType });\n }\n\n public async deleteFile(_path: string): Promise<Result<boolean>> {\n return succeed(true);\n }\n\n public async createDirectory(path: string): Promise<Result<IStorageTreeItem>> {\n return succeed({ path, name: 'created', type: 'directory' });\n }\n\n public async sync(): Promise<Result<IStorageSyncResponse>> {\n return succeed({ synced: 7 });\n }\n}\n\nclass StubProviderFactory implements IHttpStorageProviderFactory {\n private readonly _provider: IHttpStorageProvider;\n\n public constructor(provider: IHttpStorageProvider) {\n this._provider = provider;\n }\n\n public forNamespace(): Result<IHttpStorageProvider> {\n return succeed(this._provider);\n }\n}\n\nclass FailingProviderFactory implements IHttpStorageProviderFactory {\n public forNamespace(): Result<IHttpStorageProvider> {\n return fail('missing provider');\n }\n}\n\ndescribe('HttpStorageService', () => {\n test('maps getChildren response shape', async () => {\n const service = new HttpStorageService(new StubProviderFactory(new StubProvider()));\n\n const result = await service.getChildren({ path: '/data', namespace: 'default' });\n expect(result.isSuccess()).toBe(true);\n expect(result.orThrow()).toEqual({\n path: '/data',\n children: [{ path: '/data/child.txt', name: 'child.txt', type: 'file' }]\n });\n });\n\n test('formats provider lookup failures', async () => {\n const service = new HttpStorageService(new FailingProviderFactory());\n\n const getResult = await service.getFile({ path: '/missing', namespace: 'default' });\n expect(getResult.isFailure()).toBe(true);\n expect(getResult.message).toBe('provider: missing provider');\n\n const syncResult = await service.sync({ namespace: 'default' });\n expect(syncResult.isFailure()).toBe(true);\n expect(syncResult.message).toBe('provider: missing provider');\n });\n\n test('forwards deleteFile to provider', async () => {\n const service = new HttpStorageService(new StubProviderFactory(new StubProvider()));\n\n const result = await service.deleteFile({ path: '/data/remove.txt', namespace: 'default' });\n expect(result.isSuccess()).toBe(true);\n expect(result.orThrow()).toBe(true);\n });\n});\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fgv/ts-http-storage",
|
|
3
|
+
"version": "5.1.0-1",
|
|
4
|
+
"description": "Reusable HTTP storage services for FileTree-style backends",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "dist/ts-http-storage.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"node": {
|
|
10
|
+
"import": "./lib/index.js",
|
|
11
|
+
"require": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"default": {
|
|
14
|
+
"import": "./lib/index.js",
|
|
15
|
+
"require": "./lib/index.js"
|
|
16
|
+
},
|
|
17
|
+
"types": "./dist/ts-http-storage.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "heft build --clean",
|
|
22
|
+
"clean": "heft clean",
|
|
23
|
+
"test": "heft test --clean",
|
|
24
|
+
"api-report": "api-extractor run --config ./config/api-extractor.json",
|
|
25
|
+
"api-report:update": "api-extractor run --local --config ./config/api-extractor.json",
|
|
26
|
+
"build-docs": "typedoc --options ./config/typedoc.json",
|
|
27
|
+
"build-all": "rushx build; rushx build-docs",
|
|
28
|
+
"coverage": "heft test --clean --coverage",
|
|
29
|
+
"lint": "eslint src --ext .ts",
|
|
30
|
+
"fixlint": "eslint src --ext .ts --fix"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"author": "Erik Fortune",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/ErikFortune/fgv.git"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@fgv/ts-utils": "workspace:*",
|
|
41
|
+
"hono": "~4.7.11"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@microsoft/api-documenter": "^7.28.2",
|
|
45
|
+
"@microsoft/api-extractor": "^7.55.2",
|
|
46
|
+
"@fgv/ts-utils-jest": "workspace:*",
|
|
47
|
+
"@rushstack/heft": "1.2.6",
|
|
48
|
+
"@rushstack/heft-node-rig": "2.11.26",
|
|
49
|
+
"@rushstack/eslint-config": "4.6.4",
|
|
50
|
+
"@types/heft-jest": "1.0.6",
|
|
51
|
+
"@types/jest": "^29.5.14",
|
|
52
|
+
"@types/node": "^20.14.9",
|
|
53
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
54
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
55
|
+
"eslint": "^9.39.2",
|
|
56
|
+
"eslint-plugin-tsdoc": "~0.5.2",
|
|
57
|
+
"jest": "^29.7.0",
|
|
58
|
+
"ts-jest": "^29.4.6",
|
|
59
|
+
"typescript": "5.9.3",
|
|
60
|
+
"typedoc": "~0.28.16",
|
|
61
|
+
"typedoc-plugin-markdown": "~4.9.0",
|
|
62
|
+
"@rushstack/heft-jest-plugin": "1.2.6",
|
|
63
|
+
"@fgv/heft-dual-rig": "workspace:*",
|
|
64
|
+
"@fgv/typedoc-compact-theme": "workspace:*"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Invoking: heft build --clean
|
|
2
|
+
---- build started ----
|
|
3
|
+
[build:typescript] The TypeScript compiler version 5.9.3 is newer than the latest version that was tested with Heft (5.8); it may not work correctly.
|
|
4
|
+
[build:typescript] Using TypeScript version 5.9.3
|
|
5
|
+
[build:api-extractor] Using API Extractor version 7.57.6
|
|
6
|
+
[build:api-extractor] Analysis will use the bundled TypeScript version 5.8.2
|
|
7
|
+
[build:api-extractor] *** The target project appears to use TypeScript 5.9.3 which is newer than the bundled compiler engine; consider upgrading API Extractor.
|
|
8
|
+
---- build finished (2.615s) ----
|
|
9
|
+
-------------------- Finished (2.617s) --------------------
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reusable HTTP storage services for FileTree-style backends.
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export * from './packlets/storage';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { type Converter, Converters } from '@fgv/ts-utils';
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
IStorageFileResponse,
|
|
27
|
+
IStoragePathRequest,
|
|
28
|
+
IStorageSyncRequest,
|
|
29
|
+
IStorageTreeChildrenResponse,
|
|
30
|
+
IStorageTreeItem,
|
|
31
|
+
IStorageWriteFileRequest,
|
|
32
|
+
StorageItemType
|
|
33
|
+
} from './model';
|
|
34
|
+
|
|
35
|
+
const storageItemType: Converter<StorageItemType> = Converters.enumeratedValue<StorageItemType>([
|
|
36
|
+
'file',
|
|
37
|
+
'directory'
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Converter for {@link IStorageTreeItem}.
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export const storageTreeItem: Converter<IStorageTreeItem> = Converters.strictObject<IStorageTreeItem>({
|
|
45
|
+
path: Converters.string,
|
|
46
|
+
name: Converters.string,
|
|
47
|
+
type: storageItemType
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Converter for path-based requests.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export const storagePathRequest: Converter<IStoragePathRequest> =
|
|
55
|
+
Converters.strictObject<IStoragePathRequest>({
|
|
56
|
+
path: Converters.string,
|
|
57
|
+
namespace: Converters.string.optional()
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converter for write-file requests.
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export const storageWriteFileRequest: Converter<IStorageWriteFileRequest> =
|
|
65
|
+
Converters.strictObject<IStorageWriteFileRequest>({
|
|
66
|
+
path: Converters.string,
|
|
67
|
+
contents: Converters.string,
|
|
68
|
+
contentType: Converters.string.optional(),
|
|
69
|
+
namespace: Converters.string.optional()
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Converter for sync requests.
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
export const storageSyncRequest: Converter<IStorageSyncRequest> =
|
|
77
|
+
Converters.strictObject<IStorageSyncRequest>({
|
|
78
|
+
namespace: Converters.string.optional()
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Converter for file responses.
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
export const storageFileResponse: Converter<IStorageFileResponse> =
|
|
86
|
+
Converters.strictObject<IStorageFileResponse>({
|
|
87
|
+
path: Converters.string,
|
|
88
|
+
contents: Converters.string,
|
|
89
|
+
contentType: Converters.string.optional()
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Converter for children list responses.
|
|
94
|
+
* @public
|
|
95
|
+
*/
|
|
96
|
+
export const storageTreeChildrenResponse: Converter<IStorageTreeChildrenResponse> =
|
|
97
|
+
Converters.strictObject<IStorageTreeChildrenResponse>({
|
|
98
|
+
path: Converters.string,
|
|
99
|
+
children: Converters.arrayOf(storageTreeItem)
|
|
100
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from 'fs';
|
|
24
|
+
import * as fsp from 'fs/promises';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
|
|
27
|
+
import { captureResult, fail, type Result, succeed } from '@fgv/ts-utils';
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
IHttpStorageProvider,
|
|
31
|
+
IStorageFileResponse,
|
|
32
|
+
IStorageSyncResponse,
|
|
33
|
+
IStorageTreeItem,
|
|
34
|
+
StorageItemType
|
|
35
|
+
} from './model';
|
|
36
|
+
import type { IHttpStorageProviderFactory } from './provider';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options for creating filesystem-backed storage providers.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export interface IFsStorageProviderFactoryOptions {
|
|
43
|
+
readonly rootPath: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toItemType(stats: fs.Stats): StorageItemType {
|
|
47
|
+
return stats.isDirectory() ? 'directory' : 'file';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Filesystem-backed implementation of {@link IHttpStorageProvider}.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export class FsStorageProvider implements IHttpStorageProvider {
|
|
55
|
+
private readonly _rootPath: string;
|
|
56
|
+
|
|
57
|
+
public constructor(rootPath: string) {
|
|
58
|
+
this._rootPath = rootPath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async getItem(itemPath: string): Promise<Result<IStorageTreeItem>> {
|
|
62
|
+
const resolved = this._resolveAbsolutePath(itemPath);
|
|
63
|
+
if (resolved.isFailure()) {
|
|
64
|
+
return fail(resolved.message);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const stats = await fsp.stat(resolved.value);
|
|
68
|
+
return succeed(this._toTreeItem(itemPath, toItemType(stats)));
|
|
69
|
+
} catch (err: unknown) {
|
|
70
|
+
return fail(`${itemPath}: ${_toMessage(err)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async getChildren(itemPath: string): Promise<Result<ReadonlyArray<IStorageTreeItem>>> {
|
|
75
|
+
const resolved = this._resolveAbsolutePath(itemPath);
|
|
76
|
+
if (resolved.isFailure()) {
|
|
77
|
+
return fail(resolved.message);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const entries = await fsp.readdir(resolved.value, { withFileTypes: true });
|
|
81
|
+
return _mapEntries(entries, itemPath);
|
|
82
|
+
} catch (err: unknown) {
|
|
83
|
+
return fail(`${itemPath}: ${_toMessage(err)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public async getFile(itemPath: string): Promise<Result<IStorageFileResponse>> {
|
|
88
|
+
const resolved = this._resolveAbsolutePath(itemPath);
|
|
89
|
+
if (resolved.isFailure()) {
|
|
90
|
+
return fail(resolved.message);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const stats = await fsp.stat(resolved.value);
|
|
94
|
+
if (!stats.isFile()) {
|
|
95
|
+
return fail(`${itemPath}: not a file`);
|
|
96
|
+
}
|
|
97
|
+
const contents = await fsp.readFile(resolved.value, 'utf8');
|
|
98
|
+
return succeed({
|
|
99
|
+
path: normalizeRequestPath(itemPath),
|
|
100
|
+
contents
|
|
101
|
+
});
|
|
102
|
+
} catch (err: unknown) {
|
|
103
|
+
return fail(`${itemPath}: ${_toMessage(err)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async saveFile(
|
|
108
|
+
itemPath: string,
|
|
109
|
+
contents: string,
|
|
110
|
+
contentType?: string
|
|
111
|
+
): Promise<Result<IStorageFileResponse>> {
|
|
112
|
+
const resolved = this._resolveAbsolutePath(itemPath);
|
|
113
|
+
if (resolved.isFailure()) {
|
|
114
|
+
return fail(resolved.message);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const parentDir = path.dirname(resolved.value);
|
|
118
|
+
await fsp.mkdir(parentDir, { recursive: true });
|
|
119
|
+
await fsp.writeFile(resolved.value, contents, 'utf8');
|
|
120
|
+
return succeed({
|
|
121
|
+
path: normalizeRequestPath(itemPath),
|
|
122
|
+
contents,
|
|
123
|
+
contentType
|
|
124
|
+
});
|
|
125
|
+
} catch (err: unknown) {
|
|
126
|
+
return fail(`${itemPath}: ${_toMessage(err)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public async deleteFile(itemPath: string): Promise<Result<boolean>> {
|
|
131
|
+
const resolved = this._resolveAbsolutePath(itemPath);
|
|
132
|
+
if (resolved.isFailure()) {
|
|
133
|
+
return fail(resolved.message);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const stats = await fsp.stat(resolved.value);
|
|
137
|
+
if (!stats.isFile()) {
|
|
138
|
+
return fail(`${itemPath}: not a file`);
|
|
139
|
+
}
|
|
140
|
+
await fsp.unlink(resolved.value);
|
|
141
|
+
return succeed(true);
|
|
142
|
+
} catch (err: unknown) {
|
|
143
|
+
return fail(`${itemPath}: ${_toMessage(err)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public async createDirectory(itemPath: string): Promise<Result<IStorageTreeItem>> {
|
|
148
|
+
const resolved = this._resolveAbsolutePath(itemPath);
|
|
149
|
+
if (resolved.isFailure()) {
|
|
150
|
+
return fail(resolved.message);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
await fsp.mkdir(resolved.value, { recursive: true });
|
|
154
|
+
return succeed(this._toTreeItem(itemPath, 'directory'));
|
|
155
|
+
} catch (err: unknown) {
|
|
156
|
+
return fail(`${itemPath}: ${_toMessage(err)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public async sync(): Promise<Result<IStorageSyncResponse>> {
|
|
161
|
+
return succeed({ synced: 0 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _resolveAbsolutePath(requestPath: string): Result<string> {
|
|
165
|
+
const normalized = normalizeRequestPath(requestPath);
|
|
166
|
+
const candidate = path.resolve(this._rootPath, `.${normalized}`);
|
|
167
|
+
const relative = path.relative(this._rootPath, candidate);
|
|
168
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
169
|
+
return fail(`${requestPath}: path is outside storage root`);
|
|
170
|
+
}
|
|
171
|
+
return succeed(candidate);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _toTreeItem(itemPath: string, type: StorageItemType): IStorageTreeItem {
|
|
175
|
+
const normalizedPath = normalizeRequestPath(itemPath);
|
|
176
|
+
const name = normalizedPath === '/' ? '/' : path.posix.basename(normalizedPath);
|
|
177
|
+
return {
|
|
178
|
+
path: normalizedPath,
|
|
179
|
+
name,
|
|
180
|
+
type
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _mapEntries(
|
|
186
|
+
entries: ReadonlyArray<fs.Dirent>,
|
|
187
|
+
parentPath: string
|
|
188
|
+
): Result<ReadonlyArray<IStorageTreeItem>> {
|
|
189
|
+
const normalizedParent = normalizeRequestPath(parentPath);
|
|
190
|
+
const items: IStorageTreeItem[] = entries.map((entry) => {
|
|
191
|
+
const isDirectory = entry.isDirectory();
|
|
192
|
+
const childPath = normalizedParent === '/' ? `/${entry.name}` : `${normalizedParent}/${entry.name}`;
|
|
193
|
+
return {
|
|
194
|
+
path: childPath,
|
|
195
|
+
name: entry.name,
|
|
196
|
+
type: isDirectory ? 'directory' : 'file'
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
return succeed(items);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _toMessage(err: unknown): string {
|
|
203
|
+
return err instanceof Error ? err.message : String(err);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Normalizes a request path to a consistent POSIX format.
|
|
208
|
+
* @public
|
|
209
|
+
*/
|
|
210
|
+
export function normalizeRequestPath(requestPath: string): string {
|
|
211
|
+
if (requestPath.length === 0) {
|
|
212
|
+
return '/';
|
|
213
|
+
}
|
|
214
|
+
const normalized = path.posix.normalize(requestPath.startsWith('/') ? requestPath : `/${requestPath}`);
|
|
215
|
+
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Namespace-aware provider factory backed by filesystem directories.
|
|
220
|
+
* @public
|
|
221
|
+
*/
|
|
222
|
+
export class FsStorageProviderFactory implements IHttpStorageProviderFactory {
|
|
223
|
+
private readonly _rootPath: string;
|
|
224
|
+
|
|
225
|
+
public constructor(options: IFsStorageProviderFactoryOptions) {
|
|
226
|
+
this._rootPath = path.resolve(options.rootPath);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public forNamespace(namespace?: string): Result<IHttpStorageProvider> {
|
|
230
|
+
const safeNamespace = sanitizeNamespace(namespace);
|
|
231
|
+
if (safeNamespace.isFailure()) {
|
|
232
|
+
return fail(safeNamespace.message);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const namespaceRoot = path.resolve(this._rootPath, safeNamespace.value);
|
|
236
|
+
const relative = path.relative(this._rootPath, namespaceRoot);
|
|
237
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
238
|
+
return fail(`namespace '${safeNamespace.value}' is outside root`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const ensureResult = captureResult(() => fs.mkdirSync(namespaceRoot, { recursive: true }));
|
|
242
|
+
if (ensureResult.isFailure()) {
|
|
243
|
+
return fail(`namespace '${safeNamespace.value}': ${ensureResult.message}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return succeed(new FsStorageProvider(namespaceRoot));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Sanitize namespace path segment.
|
|
252
|
+
* @public
|
|
253
|
+
*/
|
|
254
|
+
export function sanitizeNamespace(namespace?: string): Result<string> {
|
|
255
|
+
if (!namespace || namespace.trim().length === 0) {
|
|
256
|
+
return succeed('default');
|
|
257
|
+
}
|
|
258
|
+
const trimmed = namespace.trim();
|
|
259
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) {
|
|
260
|
+
return fail(`namespace '${namespace}': only [a-zA-Z0-9._-] allowed`);
|
|
261
|
+
}
|
|
262
|
+
return succeed(trimmed);
|
|
263
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reusable HTTP storage services and route helpers.
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export * from './model';
|
|
29
|
+
export * from './converters';
|
|
30
|
+
export * from './provider';
|
|
31
|
+
export * from './fsProvider';
|
|
32
|
+
export * from './service';
|
|
33
|
+
export * from './routes';
|