@cepseudo/storage 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Axel Hoffmann
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.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @cepseudo/storage
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@cepseudo/storage)](https://www.npmjs.com/package/@cepseudo/storage)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../../LICENSE)
5
+
6
+ Abstract storage layer for the Digital Twin framework. Provides a unified API for persisting binary files (3D assets, collected data, tilesets) across local filesystem and S3-compatible cloud storage.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pnpm add @cepseudo/storage
12
+ ```
13
+
14
+ For S3-compatible storage (OVH, AWS, MinIO), install the AWS SDK peer dependencies:
15
+
16
+ ```bash
17
+ pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
18
+ ```
19
+
20
+ These are **optional** -- only required when using `OvhS3StorageService`. The local filesystem adapter has no additional dependencies.
21
+
22
+ ## Adapters
23
+
24
+ | Feature | `LocalStorageService` | `OvhS3StorageService` |
25
+ |---|---|---|
26
+ | Backend | Local filesystem | S3-compatible (OVH, AWS, MinIO) |
27
+ | Presigned URLs | No | Yes |
28
+ | Batch delete | Sequential | S3 `DeleteObjects` (up to 1000/request) |
29
+ | Public URLs | File path (requires static serving) | Direct HTTPS URL |
30
+ | Path traversal protection | Yes | N/A (S3 key-based) |
31
+ | CORS configuration | N/A | Built-in `configureCors()` |
32
+ | Use case | Development / testing | Production |
33
+
34
+ ## Usage
35
+
36
+ ### Creating a storage service via factory
37
+
38
+ `StorageServiceFactory` reads the `STORAGE_CONFIG` environment variable and returns the appropriate adapter.
39
+
40
+ ```typescript
41
+ import { StorageServiceFactory } from '@cepseudo/storage'
42
+
43
+ // STORAGE_CONFIG=local --> LocalStorageService
44
+ // STORAGE_CONFIG=ovh --> OvhS3StorageService (requires OVH_* env vars)
45
+ const storage = StorageServiceFactory.create()
46
+ ```
47
+
48
+ **Environment variables for `local`:**
49
+
50
+ | Variable | Default | Description |
51
+ |---|---|---|
52
+ | `STORAGE_CONFIG` | -- | Set to `local` |
53
+ | `LOCAL_STORAGE_DIR` | `data` | Base directory for file storage |
54
+
55
+ **Environment variables for `ovh`:**
56
+
57
+ | Variable | Default | Description |
58
+ |---|---|---|
59
+ | `STORAGE_CONFIG` | -- | Set to `ovh` |
60
+ | `OVH_ACCESS_KEY` | -- | S3 access key |
61
+ | `OVH_SECRET_KEY` | -- | S3 secret key |
62
+ | `OVH_ENDPOINT` | -- | S3 endpoint (e.g. `https://s3.gra.io.cloud.ovh.net`) |
63
+ | `OVH_BUCKET` | -- | Bucket name |
64
+ | `OVH_REGION` | `gra` | S3 region |
65
+
66
+ ### Direct adapter instantiation
67
+
68
+ ```typescript
69
+ import { LocalStorageService, OvhS3StorageService } from '@cepseudo/storage'
70
+
71
+ // Local filesystem
72
+ const local = new LocalStorageService('./data')
73
+
74
+ // OVH S3-compatible storage
75
+ const s3 = new OvhS3StorageService({
76
+ accessKey: 'your-access-key',
77
+ secretKey: 'your-secret-key',
78
+ endpoint: 'https://s3.gra.io.cloud.ovh.net',
79
+ bucket: 'my-bucket',
80
+ region: 'gra'
81
+ })
82
+ ```
83
+
84
+ ### Storing and retrieving files
85
+
86
+ ```typescript
87
+ // Store with auto-generated timestamp filename
88
+ const key = await storage.save(buffer, 'weather-sensor', 'json')
89
+ // --> 'weather-sensor/2026-03-06T10-30-00-000Z.json'
90
+
91
+ // Store at a specific path (preserves filename)
92
+ await storage.saveWithPath(buffer, 'tilesets/42/tileset.json')
93
+
94
+ // Retrieve
95
+ const data = await storage.retrieve(key)
96
+
97
+ // Delete
98
+ await storage.delete(key)
99
+
100
+ // Delete in batch (S3 adapter uses optimized bulk delete)
101
+ await storage.deleteBatch(['path/a.json', 'path/b.json'])
102
+
103
+ // Delete all files under a prefix
104
+ const count = await storage.deleteByPrefix('tilesets/42')
105
+
106
+ // Get public URL
107
+ const url = storage.getPublicUrl('tilesets/42/tileset.json')
108
+ // --> 'https://my-bucket.s3.gra.io.cloud.ovh.net/tilesets/42/tileset.json'
109
+ ```
110
+
111
+ ### Presigned URL upload flow
112
+
113
+ Presigned URLs allow clients to upload files directly to S3, bypassing the backend server entirely. This is essential for large files (3D assets, tilesets).
114
+
115
+ ```typescript
116
+ // 1. Check if the storage backend supports presigned URLs
117
+ if (!storage.supportsPresignedUrls()) {
118
+ throw new Error('Storage backend does not support presigned URLs')
119
+ }
120
+
121
+ // 2. Generate a presigned PUT URL (valid for 5 minutes by default)
122
+ const { url, key, expiresAt } = await storage.generatePresignedUploadUrl(
123
+ 'assets/uploads/model.glb', // target key
124
+ 'model/gltf-binary', // content type
125
+ 300 // expiry in seconds (optional, default 300)
126
+ )
127
+
128
+ // 3. Return url + key to the client; client uploads directly via HTTP PUT
129
+
130
+ // 4. After upload, verify the file exists in storage
131
+ const { exists, contentLength, contentType } = await storage.objectExists(key)
132
+ ```
133
+
134
+ ### CORS configuration (S3 only)
135
+
136
+ For browser-based uploads via presigned URLs, the S3 bucket needs CORS rules. The factory configures this automatically, but you can also call it manually:
137
+
138
+ ```typescript
139
+ await s3.configureCors(
140
+ ['https://your-domain.be'], // allowed origins
141
+ ['GET', 'HEAD', 'PUT', 'POST'], // allowed methods
142
+ ['*', 'Authorization'] // allowed headers
143
+ )
144
+ ```
145
+
146
+ ## API Reference
147
+
148
+ ### `StorageService` (abstract)
149
+
150
+ | Method | Returns | Description |
151
+ |---|---|---|
152
+ | `save(buffer, collectorName, extension?)` | `Promise<string>` | Store with auto-generated key |
153
+ | `saveWithPath(buffer, relativePath)` | `Promise<string>` | Store at exact path |
154
+ | `retrieve(path)` | `Promise<Buffer>` | Read file contents |
155
+ | `delete(path)` | `Promise<void>` | Delete single file |
156
+ | `deleteBatch(paths)` | `Promise<void>` | Delete multiple files |
157
+ | `deleteByPrefix(prefix)` | `Promise<number>` | Delete all files under prefix |
158
+ | `getPublicUrl(relativePath)` | `string` | Get public URL for file |
159
+ | `supportsPresignedUrls()` | `boolean` | Whether presigned URLs are supported |
160
+ | `generatePresignedUploadUrl(key, contentType, expiresIn?)` | `Promise<PresignedUploadResult>` | Generate presigned PUT URL |
161
+ | `objectExists(key)` | `Promise<ObjectExistsResult>` | Check if object exists with metadata |
162
+
163
+ ### Types
164
+
165
+ ```typescript
166
+ interface PresignedUploadResult {
167
+ url: string // presigned PUT URL
168
+ key: string // object key in storage
169
+ expiresAt: Date // URL expiration timestamp
170
+ }
171
+
172
+ interface ObjectExistsResult {
173
+ exists: boolean
174
+ contentLength?: number // file size in bytes
175
+ contentType?: string // MIME type
176
+ }
177
+
178
+ interface OvhS3Config {
179
+ accessKey: string
180
+ secretKey: string
181
+ endpoint: string // e.g. 'https://s3.gra.io.cloud.ovh.net'
182
+ region?: string // e.g. 'gra' (default)
183
+ bucket: string
184
+ }
185
+ ```
186
+
187
+ ## Peer Dependencies
188
+
189
+ | Package | Required for | Required? |
190
+ |---|---|---|
191
+ | `@aws-sdk/client-s3` >= 3.0.0 | `OvhS3StorageService` | Optional |
192
+ | `@aws-sdk/s3-request-presigner` >= 3.0.0 | Presigned URL generation | Optional |
193
+
194
+ The AWS SDK packages are only loaded by `OvhS3StorageService`. If you only use `LocalStorageService`, they are not needed.
195
+
196
+ ## License
197
+
198
+ MIT
@@ -0,0 +1,57 @@
1
+ import { StorageService } from '../storage_service.js';
2
+ /**
3
+ * Local filesystem-based implementation of the StorageService.
4
+ * Saves files in a configured folder using a timestamp as filename.
5
+ */
6
+ export declare class LocalStorageService extends StorageService {
7
+ #private;
8
+ private baseDir;
9
+ constructor(baseDir?: string);
10
+ /**
11
+ * Saves the given buffer to disk under a unique filename.
12
+ * @param buffer - Content to save
13
+ * @param collectorName - Name of the collector (used for folder)
14
+ * @param extension - Optional file extension (e.g. 'json', 'txt')
15
+ * @returns Relative path to the saved file
16
+ */
17
+ save(buffer: Buffer, collectorName: string, extension?: string): Promise<string>;
18
+ /**
19
+ * Retrieves a file as buffer using its relative path.
20
+ * @param relativePath - Filename previously returned by `save`
21
+ * @returns File content as Buffer
22
+ * @throws Error if path traversal is detected
23
+ */
24
+ retrieve(relativePath: string): Promise<Buffer>;
25
+ /**
26
+ * Deletes a stored file.
27
+ * @param relativePath - Filename previously returned by `save`
28
+ * @throws Error if path traversal is detected
29
+ */
30
+ delete(relativePath: string): Promise<void>;
31
+ /**
32
+ * Saves the given buffer to disk at a specific path (preserves filename).
33
+ * Unlike save(), this method does not auto-generate a timestamp filename.
34
+ * @param buffer - Content to save
35
+ * @param relativePath - Full relative path including filename (e.g., 'tilesets/123/tileset.json')
36
+ * @returns The same relative path that was provided
37
+ * @throws Error if path traversal is detected
38
+ */
39
+ saveWithPath(buffer: Buffer, relativePath: string): Promise<string>;
40
+ /**
41
+ * Returns a local file path for the stored file.
42
+ * Note: For local storage, this returns a relative file path, not an HTTP URL.
43
+ * In production, use a cloud storage service (OVH, S3) for public URLs.
44
+ * @param relativePath - The storage path of the file
45
+ * @returns The file path (relative to baseDir)
46
+ * @throws Error if path traversal is detected
47
+ */
48
+ getPublicUrl(relativePath: string): string;
49
+ /**
50
+ * Deletes all files under a given prefix (folder).
51
+ * @param prefix - The folder/prefix to delete (e.g., 'tilesets/123')
52
+ * @returns Number of files deleted
53
+ * @throws Error if path traversal is detected
54
+ */
55
+ deleteByPrefix(prefix: string): Promise<number>;
56
+ }
57
+ //# sourceMappingURL=local_storage_service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local_storage_service.d.ts","sourceRoot":"","sources":["../../src/adapters/local_storage_service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAItD;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,cAAc;;IAGvC,OAAO,CAAC,OAAO;gBAAP,OAAO,GAAE,MAAe;IAqB5C;;;;;;OAMG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAetF;;;;;OAKG;IACG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKrD;;;;OAIG;IACG,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjD;;;;;;;OAOG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUzE;;;;;;;OAOG;IACH,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAQ1C;;;;;OAKG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAgCxD"}
@@ -0,0 +1,132 @@
1
+ import { StorageService } from '../storage_service.js';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ /**
5
+ * Local filesystem-based implementation of the StorageService.
6
+ * Saves files in a configured folder using a timestamp as filename.
7
+ */
8
+ export class LocalStorageService extends StorageService {
9
+ #normalizedBase;
10
+ constructor(baseDir = 'data') {
11
+ super();
12
+ this.baseDir = baseDir;
13
+ this.#normalizedBase = path.resolve(this.baseDir);
14
+ }
15
+ /**
16
+ * Validates that a path does not escape the base directory (path traversal protection).
17
+ * @param relativePath - The relative path to validate
18
+ * @returns The resolved absolute path if valid
19
+ * @throws Error if path traversal is detected
20
+ */
21
+ #validatePath(relativePath) {
22
+ const resolved = path.resolve(this.#normalizedBase, relativePath);
23
+ if (!resolved.startsWith(this.#normalizedBase + path.sep) && resolved !== this.#normalizedBase) {
24
+ throw new Error(`Invalid path: path traversal detected for "${relativePath}"`);
25
+ }
26
+ return resolved;
27
+ }
28
+ /**
29
+ * Saves the given buffer to disk under a unique filename.
30
+ * @param buffer - Content to save
31
+ * @param collectorName - Name of the collector (used for folder)
32
+ * @param extension - Optional file extension (e.g. 'json', 'txt')
33
+ * @returns Relative path to the saved file
34
+ */
35
+ async save(buffer, collectorName, extension) {
36
+ const now = new Date();
37
+ const timestamp = now.toISOString().replace(/[:.]/g, '-');
38
+ const folder = collectorName || 'default';
39
+ const filename = extension ? `${timestamp}.${extension}` : timestamp;
40
+ const dirPath = path.join(this.baseDir, folder);
41
+ const filePath = path.join(dirPath, filename);
42
+ await fs.mkdir(dirPath, { recursive: true });
43
+ await fs.writeFile(filePath, buffer);
44
+ // return relative path (e.g., 'mycollector/2025-07-07T15-45-22-456Z.json')
45
+ return path.join(folder, filename);
46
+ }
47
+ /**
48
+ * Retrieves a file as buffer using its relative path.
49
+ * @param relativePath - Filename previously returned by `save`
50
+ * @returns File content as Buffer
51
+ * @throws Error if path traversal is detected
52
+ */
53
+ async retrieve(relativePath) {
54
+ const filePath = this.#validatePath(relativePath);
55
+ return fs.readFile(filePath);
56
+ }
57
+ /**
58
+ * Deletes a stored file.
59
+ * @param relativePath - Filename previously returned by `save`
60
+ * @throws Error if path traversal is detected
61
+ */
62
+ async delete(relativePath) {
63
+ const filePath = this.#validatePath(relativePath);
64
+ await fs.rm(filePath, { force: true });
65
+ }
66
+ /**
67
+ * Saves the given buffer to disk at a specific path (preserves filename).
68
+ * Unlike save(), this method does not auto-generate a timestamp filename.
69
+ * @param buffer - Content to save
70
+ * @param relativePath - Full relative path including filename (e.g., 'tilesets/123/tileset.json')
71
+ * @returns The same relative path that was provided
72
+ * @throws Error if path traversal is detected
73
+ */
74
+ async saveWithPath(buffer, relativePath) {
75
+ const filePath = this.#validatePath(relativePath);
76
+ const dirPath = path.dirname(filePath);
77
+ await fs.mkdir(dirPath, { recursive: true });
78
+ await fs.writeFile(filePath, buffer);
79
+ return relativePath;
80
+ }
81
+ /**
82
+ * Returns a local file path for the stored file.
83
+ * Note: For local storage, this returns a relative file path, not an HTTP URL.
84
+ * In production, use a cloud storage service (OVH, S3) for public URLs.
85
+ * @param relativePath - The storage path of the file
86
+ * @returns The file path (relative to baseDir)
87
+ * @throws Error if path traversal is detected
88
+ */
89
+ getPublicUrl(relativePath) {
90
+ // Validate path to prevent traversal (even though this just returns a string)
91
+ this.#validatePath(relativePath);
92
+ // For local storage, return the file path
93
+ // In a real deployment, you'd need Express static serving or similar
94
+ return path.join(this.baseDir, relativePath);
95
+ }
96
+ /**
97
+ * Deletes all files under a given prefix (folder).
98
+ * @param prefix - The folder/prefix to delete (e.g., 'tilesets/123')
99
+ * @returns Number of files deleted
100
+ * @throws Error if path traversal is detected
101
+ */
102
+ async deleteByPrefix(prefix) {
103
+ const folderPath = this.#validatePath(prefix);
104
+ try {
105
+ // Check if folder exists
106
+ await fs.access(folderPath);
107
+ // Count files before deletion
108
+ const countFiles = async (dir) => {
109
+ let count = 0;
110
+ const entries = await fs.readdir(dir, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ if (entry.isDirectory()) {
113
+ count += await countFiles(path.join(dir, entry.name));
114
+ }
115
+ else {
116
+ count++;
117
+ }
118
+ }
119
+ return count;
120
+ };
121
+ const fileCount = await countFiles(folderPath);
122
+ // Delete folder recursively
123
+ await fs.rm(folderPath, { recursive: true, force: true });
124
+ return fileCount;
125
+ }
126
+ catch {
127
+ // Folder doesn't exist
128
+ return 0;
129
+ }
130
+ }
131
+ }
132
+ //# sourceMappingURL=local_storage_service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local_storage_service.js","sourceRoot":"","sources":["../../src/adapters/local_storage_service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import { StorageService } from '../storage_service.js';
2
+ import type { PresignedUploadResult, ObjectExistsResult } from '../storage_service.js';
3
+ export interface OvhS3Config {
4
+ accessKey: string;
5
+ secretKey: string;
6
+ endpoint: string;
7
+ region?: string;
8
+ bucket: string;
9
+ pathStyle?: boolean;
10
+ }
11
+ export declare class OvhS3StorageService extends StorageService {
12
+ #private;
13
+ constructor(config: OvhS3Config);
14
+ /**
15
+ * Uploads a file to the OVH S3-compatible object storage.
16
+ * @param buffer - File contents to upload
17
+ * @param collectorName - Folder/prefix to store under
18
+ * @param extension - Optional file extension (e.g. 'json')
19
+ * @returns The relative path (key) of the stored object
20
+ */
21
+ save(buffer: Buffer, collectorName: string, extension?: string): Promise<string>;
22
+ /**
23
+ * Downloads and returns a stored object as a Buffer.
24
+ * @param relativePath - The key/path of the object to retrieve
25
+ * @returns The object contents as a Buffer
26
+ */
27
+ retrieve(relativePath: string): Promise<Buffer>;
28
+ /**
29
+ * Deletes an object from the storage bucket.
30
+ * @param relativePath - The key/path of the object to delete
31
+ */
32
+ delete(relativePath: string): Promise<void>;
33
+ /**
34
+ * Uploads a file to OVH S3 at a specific path (preserves filename).
35
+ * Unlike save(), this method does not auto-generate a timestamp filename.
36
+ * Files are uploaded with public-read ACL for direct access (e.g., Cesium tilesets).
37
+ * @param buffer - File contents to upload
38
+ * @param relativePath - Full relative path including filename (e.g., 'tilesets/123/tileset.json')
39
+ * @returns The same relative path that was provided
40
+ */
41
+ saveWithPath(buffer: Buffer, relativePath: string): Promise<string>;
42
+ /**
43
+ * Deletes multiple objects in batch using S3 DeleteObjects API.
44
+ * Much faster than individual deletes - can delete up to 1000 objects per request.
45
+ * @param paths - Array of object keys to delete
46
+ */
47
+ deleteBatch(paths: string[]): Promise<void>;
48
+ /**
49
+ * Returns the public URL for a stored file.
50
+ * Constructs the OVH S3 public URL format: https://{bucket}.{endpoint_host}/{key}
51
+ * @param relativePath - The storage path/key of the file
52
+ * @returns The public URL to access the file directly
53
+ */
54
+ getPublicUrl(relativePath: string): string;
55
+ /**
56
+ * Deletes all objects under a given prefix (folder).
57
+ * Lists objects by prefix and deletes them in batches for performance.
58
+ * @param prefix - The folder/prefix to delete (e.g., 'tilesets/123')
59
+ * @returns Number of files deleted
60
+ */
61
+ deleteByPrefix(prefix: string): Promise<number>;
62
+ /**
63
+ * This storage backend supports presigned URLs for direct client uploads.
64
+ */
65
+ supportsPresignedUrls(): boolean;
66
+ /**
67
+ * Generate a presigned PUT URL for direct client-to-S3 uploads.
68
+ */
69
+ generatePresignedUploadUrl(key: string, contentType: string, expiresInSeconds?: number): Promise<PresignedUploadResult>;
70
+ /**
71
+ * Check if an object exists in the S3 bucket using HeadObject.
72
+ */
73
+ objectExists(key: string): Promise<ObjectExistsResult>;
74
+ /**
75
+ * Configure CORS settings for the bucket.
76
+ * Required for browser-based access to public files (e.g., Cesium loading tilesets).
77
+ * Should be called once during application startup.
78
+ *
79
+ * @param allowedOrigins - List of allowed origins (default: ['*'])
80
+ * @param allowedMethods - List of allowed HTTP methods (default: ['GET', 'HEAD'])
81
+ * @param allowedHeaders - List of allowed headers (default: ['*', 'Authorization'])
82
+ * @returns true if successful, false otherwise
83
+ */
84
+ configureCors(allowedOrigins?: string[], allowedMethods?: string[], allowedHeaders?: string[]): Promise<boolean>;
85
+ }
86
+ //# sourceMappingURL=ovh_storage_service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ovh_storage_service.d.ts","sourceRoot":"","sources":["../../src/adapters/ovh_storage_service.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAMtF,MAAM,WAAW,WAAW;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,qBAAa,mBAAoB,SAAQ,cAAc;;gBAKvC,MAAM,EAAE,WAAW;IAkB/B;;;;;;OAMG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBtF;;;;OAIG;IACG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBrD;;;OAGG;IACG,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASjD;;;;;;;OAOG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAazE;;;;OAIG;IACY,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAoC1D;;;;;OAKG;IACH,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAO1C;;;;;OAKG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0CrD;;OAEG;IACM,qBAAqB,IAAI,OAAO;IAIzC;;OAEG;IACY,0BAA0B,CACrC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,gBAAgB,GAAE,MAAY,GAC/B,OAAO,CAAC,qBAAqB,CAAC;IAajC;;OAEG;IACY,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAsBrE;;;;;;;;;OASG;IACG,aAAa,CACf,cAAc,GAAE,MAAM,EAAU,EAChC,cAAc,GAAE,MAAM,EAA2B,EACjD,cAAc,GAAE,MAAM,EAA2B,GAClD,OAAO,CAAC,OAAO,CAAC;CAyBtB"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * OVH Object Storage implementation of StorageService
3
+ * via S3-compatible API using @aws-sdk/client-s3
4
+ */
5
+ import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, ListObjectsV2Command, HeadObjectCommand, PutBucketCorsCommand, ObjectCannedACL } from '@aws-sdk/client-s3';
6
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
7
+ import { StorageService } from '../storage_service.js';
8
+ import { safeAsync, Logger } from '@cepseudo/shared';
9
+ const logger = new Logger('OvhS3Storage');
10
+ export class OvhS3StorageService extends StorageService {
11
+ #s3;
12
+ #bucket;
13
+ #endpoint;
14
+ constructor(config) {
15
+ super();
16
+ this.#bucket = config.bucket;
17
+ this.#endpoint = config.endpoint;
18
+ this.#s3 = new S3Client({
19
+ endpoint: config.endpoint,
20
+ region: config.region ?? 'gra',
21
+ credentials: {
22
+ accessKeyId: config.accessKey,
23
+ secretAccessKey: config.secretKey
24
+ },
25
+ forcePathStyle: config.pathStyle ?? false,
26
+ // Match Python boto3 config for OVH compatibility
27
+ requestChecksumCalculation: 'WHEN_REQUIRED',
28
+ responseChecksumValidation: 'WHEN_REQUIRED'
29
+ });
30
+ }
31
+ /**
32
+ * Uploads a file to the OVH S3-compatible object storage.
33
+ * @param buffer - File contents to upload
34
+ * @param collectorName - Folder/prefix to store under
35
+ * @param extension - Optional file extension (e.g. 'json')
36
+ * @returns The relative path (key) of the stored object
37
+ */
38
+ async save(buffer, collectorName, extension) {
39
+ const now = new Date();
40
+ const timestamp = now.toISOString().replace(/[:.]/g, '-');
41
+ const key = `${collectorName || 'default'}/${timestamp}${extension ? '.' + extension : ''}`;
42
+ await this.#s3.send(new PutObjectCommand({
43
+ Bucket: this.#bucket,
44
+ Key: key,
45
+ Body: buffer,
46
+ ACL: ObjectCannedACL.private
47
+ }));
48
+ return key;
49
+ }
50
+ /**
51
+ * Downloads and returns a stored object as a Buffer.
52
+ * @param relativePath - The key/path of the object to retrieve
53
+ * @returns The object contents as a Buffer
54
+ */
55
+ async retrieve(relativePath) {
56
+ const res = await this.#s3.send(new GetObjectCommand({
57
+ Bucket: this.#bucket,
58
+ Key: relativePath
59
+ }));
60
+ const chunks = [];
61
+ const stream = res.Body;
62
+ for await (const chunk of stream) {
63
+ chunks.push(Buffer.from(chunk));
64
+ }
65
+ return Buffer.concat(chunks);
66
+ }
67
+ /**
68
+ * Deletes an object from the storage bucket.
69
+ * @param relativePath - The key/path of the object to delete
70
+ */
71
+ async delete(relativePath) {
72
+ await this.#s3.send(new DeleteObjectCommand({
73
+ Bucket: this.#bucket,
74
+ Key: relativePath
75
+ }));
76
+ }
77
+ /**
78
+ * Uploads a file to OVH S3 at a specific path (preserves filename).
79
+ * Unlike save(), this method does not auto-generate a timestamp filename.
80
+ * Files are uploaded with public-read ACL for direct access (e.g., Cesium tilesets).
81
+ * @param buffer - File contents to upload
82
+ * @param relativePath - Full relative path including filename (e.g., 'tilesets/123/tileset.json')
83
+ * @returns The same relative path that was provided
84
+ */
85
+ async saveWithPath(buffer, relativePath) {
86
+ await this.#s3.send(new PutObjectCommand({
87
+ Bucket: this.#bucket,
88
+ Key: relativePath,
89
+ Body: buffer,
90
+ ACL: ObjectCannedACL.public_read
91
+ }));
92
+ return relativePath;
93
+ }
94
+ /**
95
+ * Deletes multiple objects in batch using S3 DeleteObjects API.
96
+ * Much faster than individual deletes - can delete up to 1000 objects per request.
97
+ * @param paths - Array of object keys to delete
98
+ */
99
+ async deleteBatch(paths) {
100
+ if (paths.length === 0)
101
+ return;
102
+ // S3 DeleteObjects supports max 1000 objects per request
103
+ const BATCH_SIZE = 1000;
104
+ const batches = [];
105
+ for (let i = 0; i < paths.length; i += BATCH_SIZE) {
106
+ batches.push(paths.slice(i, i + BATCH_SIZE));
107
+ }
108
+ // Process batches in parallel (but limit concurrency to avoid overwhelming the API)
109
+ const MAX_CONCURRENT = 5;
110
+ for (let i = 0; i < batches.length; i += MAX_CONCURRENT) {
111
+ const concurrentBatches = batches.slice(i, i + MAX_CONCURRENT);
112
+ await Promise.all(concurrentBatches.map((batch, index) => safeAsync(() => this.#s3.send(new DeleteObjectsCommand({
113
+ Bucket: this.#bucket,
114
+ Delete: {
115
+ Objects: batch.map(key => ({ Key: key })),
116
+ Quiet: true // Don't return info about each deleted object
117
+ }
118
+ })), `delete batch ${i + index + 1}/${batches.length}`, logger)));
119
+ }
120
+ }
121
+ /**
122
+ * Returns the public URL for a stored file.
123
+ * Constructs the OVH S3 public URL format: https://{bucket}.{endpoint_host}/{key}
124
+ * @param relativePath - The storage path/key of the file
125
+ * @returns The public URL to access the file directly
126
+ */
127
+ getPublicUrl(relativePath) {
128
+ // Extract host from endpoint (e.g., 'https://s3.gra.io.cloud.ovh.net' -> 's3.gra.io.cloud.ovh.net')
129
+ const endpointHost = this.#endpoint.replace(/^https?:\/\//, '');
130
+ // OVH S3 URL format: https://{bucket}.{endpoint_host}/{key}
131
+ return `https://${this.#bucket}.${endpointHost}/${relativePath}`;
132
+ }
133
+ /**
134
+ * Deletes all objects under a given prefix (folder).
135
+ * Lists objects by prefix and deletes them in batches for performance.
136
+ * @param prefix - The folder/prefix to delete (e.g., 'tilesets/123')
137
+ * @returns Number of files deleted
138
+ */
139
+ async deleteByPrefix(prefix) {
140
+ let totalDeleted = 0;
141
+ let continuationToken;
142
+ // Ensure prefix ends with '/' to avoid partial matches
143
+ const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
144
+ do {
145
+ // List objects with prefix (max 1000 per request)
146
+ const listResponse = await this.#s3.send(new ListObjectsV2Command({
147
+ Bucket: this.#bucket,
148
+ Prefix: normalizedPrefix,
149
+ ContinuationToken: continuationToken
150
+ }));
151
+ const objects = listResponse.Contents || [];
152
+ if (objects.length === 0)
153
+ break;
154
+ // Delete objects in batch
155
+ const keys = objects.map(obj => obj.Key).filter((key) => !!key);
156
+ if (keys.length > 0) {
157
+ await this.#s3.send(new DeleteObjectsCommand({
158
+ Bucket: this.#bucket,
159
+ Delete: {
160
+ Objects: keys.map(key => ({ Key: key })),
161
+ Quiet: true
162
+ }
163
+ }));
164
+ totalDeleted += keys.length;
165
+ }
166
+ continuationToken = listResponse.NextContinuationToken;
167
+ } while (continuationToken);
168
+ return totalDeleted;
169
+ }
170
+ /**
171
+ * This storage backend supports presigned URLs for direct client uploads.
172
+ */
173
+ supportsPresignedUrls() {
174
+ return true;
175
+ }
176
+ /**
177
+ * Generate a presigned PUT URL for direct client-to-S3 uploads.
178
+ */
179
+ async generatePresignedUploadUrl(key, contentType, expiresInSeconds = 300) {
180
+ const command = new PutObjectCommand({
181
+ Bucket: this.#bucket,
182
+ Key: key,
183
+ ContentType: contentType
184
+ });
185
+ const url = await getSignedUrl(this.#s3, command, { expiresIn: expiresInSeconds });
186
+ const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
187
+ return { url, key, expiresAt };
188
+ }
189
+ /**
190
+ * Check if an object exists in the S3 bucket using HeadObject.
191
+ */
192
+ async objectExists(key) {
193
+ try {
194
+ const response = await this.#s3.send(new HeadObjectCommand({
195
+ Bucket: this.#bucket,
196
+ Key: key
197
+ }));
198
+ return {
199
+ exists: true,
200
+ contentLength: response.ContentLength,
201
+ contentType: response.ContentType
202
+ };
203
+ }
204
+ catch (error) {
205
+ const name = error?.name;
206
+ if (name === 'NotFound' || name === 'NoSuchKey') {
207
+ return { exists: false };
208
+ }
209
+ throw error;
210
+ }
211
+ }
212
+ /**
213
+ * Configure CORS settings for the bucket.
214
+ * Required for browser-based access to public files (e.g., Cesium loading tilesets).
215
+ * Should be called once during application startup.
216
+ *
217
+ * @param allowedOrigins - List of allowed origins (default: ['*'])
218
+ * @param allowedMethods - List of allowed HTTP methods (default: ['GET', 'HEAD'])
219
+ * @param allowedHeaders - List of allowed headers (default: ['*', 'Authorization'])
220
+ * @returns true if successful, false otherwise
221
+ */
222
+ async configureCors(allowedOrigins = ['*'], allowedMethods = ['GET', 'HEAD', 'PUT'], allowedHeaders = ['*', 'Authorization']) {
223
+ try {
224
+ await this.#s3.send(new PutBucketCorsCommand({
225
+ Bucket: this.#bucket,
226
+ CORSConfiguration: {
227
+ CORSRules: [
228
+ {
229
+ AllowedOrigins: allowedOrigins,
230
+ AllowedMethods: allowedMethods,
231
+ AllowedHeaders: allowedHeaders,
232
+ ExposeHeaders: ['ETag', 'Content-Length'],
233
+ MaxAgeSeconds: 3000
234
+ }
235
+ ]
236
+ }
237
+ }));
238
+ console.log('[OvhS3StorageService] CORS configured successfully');
239
+ return true;
240
+ }
241
+ catch (error) {
242
+ console.error('[OvhS3StorageService] Error configuring CORS:', error);
243
+ return false;
244
+ }
245
+ }
246
+ }
247
+ //# sourceMappingURL=ovh_storage_service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ovh_storage_service.js","sourceRoot":"","sources":["../../src/adapters/ovh_storage_service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { StorageService } from './storage_service.js';
2
+ export type { PresignedUploadResult, ObjectExistsResult } from './storage_service.js';
3
+ export { StorageServiceFactory } from './storage_factory.js';
4
+ export { LocalStorageService } from './adapters/local_storage_service.js';
5
+ export { OvhS3StorageService } from './adapters/ovh_storage_service.js';
6
+ export type { OvhS3Config } from './adapters/ovh_storage_service.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,YAAY,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACrF,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Storage service base class and factory
2
+ export { StorageService } from './storage_service.js';
3
+ export { StorageServiceFactory } from './storage_factory.js';
4
+ // Storage adapters
5
+ export { LocalStorageService } from './adapters/local_storage_service.js';
6
+ export { OvhS3StorageService } from './adapters/ovh_storage_service.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,mBAAmB;AACnB,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA"}
@@ -0,0 +1,14 @@
1
+ import type { StorageService } from './storage_service.js';
2
+ export declare class StorageServiceFactory {
3
+ /**
4
+ * Creates and returns an instance of StorageService
5
+ * based on the STORAGE_CONFIG environment variable.
6
+ *
7
+ * - 'local': returns a LocalStorageService
8
+ * - 'ovh': returns an OvhS3StorageService
9
+ *
10
+ * @throws Error if STORAGE_CONFIG is not supported
11
+ */
12
+ static create(): StorageService;
13
+ }
14
+ //# sourceMappingURL=storage_factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage_factory.d.ts","sourceRoot":"","sources":["../src/storage_factory.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAI1D,qBAAa,qBAAqB;IAC9B;;;;;;;;OAQG;IACH,MAAM,CAAC,MAAM,IAAI,cAAc;CA4BlC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Factory class for creating the appropriate StorageService
3
+ * implementation based on environment configuration.
4
+ */
5
+ import { Env, safeAsync, Logger } from '@cepseudo/shared';
6
+ import { OvhS3StorageService } from './adapters/ovh_storage_service.js';
7
+ import { LocalStorageService } from './adapters/local_storage_service.js';
8
+ const logger = new Logger('StorageFactory');
9
+ export class StorageServiceFactory {
10
+ /**
11
+ * Creates and returns an instance of StorageService
12
+ * based on the STORAGE_CONFIG environment variable.
13
+ *
14
+ * - 'local': returns a LocalStorageService
15
+ * - 'ovh': returns an OvhS3StorageService
16
+ *
17
+ * @throws Error if STORAGE_CONFIG is not supported
18
+ */
19
+ static create() {
20
+ const env = Env.config;
21
+ switch (env.STORAGE_CONFIG) {
22
+ case 'local':
23
+ return new LocalStorageService(env.LOCAL_STORAGE_DIR || 'data');
24
+ case 'ovh': {
25
+ const ovhStorage = new OvhS3StorageService({
26
+ accessKey: env.OVH_ACCESS_KEY,
27
+ secretKey: env.OVH_SECRET_KEY,
28
+ endpoint: env.OVH_ENDPOINT,
29
+ bucket: env.OVH_BUCKET,
30
+ region: env.OVH_REGION ?? 'gra'
31
+ });
32
+ // Configure CORS for browser access (non-blocking)
33
+ safeAsync(() => ovhStorage.configureCors(['*'], ['GET', 'HEAD', 'PUT'], ['*', 'Authorization']), 'configure OVH CORS', logger);
34
+ return ovhStorage;
35
+ }
36
+ default:
37
+ throw new Error(`Unsupported STORAGE_CONFIG: ${env.STORAGE_CONFIG}`);
38
+ }
39
+ }
40
+ }
41
+ //# sourceMappingURL=storage_factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage_factory.js","sourceRoot":"","sources":["../src/storage_factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA;AACvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AAGzE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,gBAAgB,CAAC,CAAA;AAE3C,MAAM,OAAO,qBAAqB;IAC9B;;;;;;;;OAQG;IACH,MAAM,CAAC,MAAM;QACT,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAA;QAEtB,QAAQ,GAAG,CAAC,cAAc,EAAE,CAAC;YACzB,KAAK,OAAO;gBACR,OAAO,IAAI,mBAAmB,CAAC,GAAG,CAAC,iBAAiB,IAAI,MAAM,CAAC,CAAA;YAEnE,KAAK,KAAK,CAAC,CAAC,CAAC;gBACT,MAAM,UAAU,GAAG,IAAI,mBAAmB,CAAC;oBACvC,SAAS,EAAE,GAAG,CAAC,cAAc;oBAC7B,SAAS,EAAE,GAAG,CAAC,cAAc;oBAC7B,QAAQ,EAAE,GAAG,CAAC,YAAY;oBAC1B,MAAM,EAAE,GAAG,CAAC,UAAU;oBACtB,MAAM,EAAE,GAAG,CAAC,UAAU,IAAI,KAAK;iBAClC,CAAC,CAAA;gBACF,mDAAmD;gBACnD,SAAS,CACL,GAAG,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC,EACrF,oBAAoB,EACpB,MAAM,CACT,CAAA;gBACD,OAAO,UAAU,CAAA;YACrB,CAAC;YAED;gBACI,MAAM,IAAI,KAAK,CAAC,+BAA+B,GAAG,CAAC,cAAc,EAAE,CAAC,CAAA;QAC5E,CAAC;IACL,CAAC;CACJ"}
@@ -0,0 +1,196 @@
1
+ export interface PresignedUploadResult {
2
+ url: string;
3
+ key: string;
4
+ expiresAt: Date;
5
+ }
6
+ export interface ObjectExistsResult {
7
+ exists: boolean;
8
+ contentLength?: number;
9
+ contentType?: string;
10
+ }
11
+ /**
12
+ * Abstract base class for storage service implementations.
13
+ *
14
+ * Defines the contract for persisting and retrieving binary data in the Digital Twin framework.
15
+ * Concrete implementations provide storage backends like local filesystem, AWS S3, Azure Blob, etc.
16
+ *
17
+ * @abstract
18
+ * @class StorageService
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // Implement for specific storage backend
23
+ * class S3StorageService extends StorageService {
24
+ * async save(buffer: Buffer, collectorName: string, extension?: string): Promise<string> {
25
+ * // Upload to S3 bucket
26
+ * return 's3://bucket/path/to/file'
27
+ * }
28
+ *
29
+ * async retrieve(path: string): Promise<Buffer> {
30
+ * // Download from S3
31
+ * return buffer
32
+ * }
33
+ *
34
+ * async delete(path: string): Promise<void> {
35
+ * // Delete from S3
36
+ * }
37
+ * }
38
+ * ```
39
+ */
40
+ export declare abstract class StorageService {
41
+ /**
42
+ * Persists binary data and returns a unique identifier for retrieval.
43
+ *
44
+ * The storage implementation should ensure the returned path/URL is unique
45
+ * and can be used later to retrieve the exact same data.
46
+ *
47
+ * @abstract
48
+ * @param {Buffer} buffer - Binary data to store
49
+ * @param {string} collectorName - Component name for organizing storage (used as folder/prefix)
50
+ * @param {string} extension - Optional file extension for proper content handling
51
+ * @returns {Promise<string>} Unique storage identifier (path, URL, or key)
52
+ * @throws {Error} When storage operation fails
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const buffer = Buffer.from('{"temperature": 23.5}')
57
+ * const path = await storage.save(buffer, 'weather-sensor', 'json')
58
+ * // Returns: '/storage/weather-sensor/2024-01-15_14-30-00.json'
59
+ * ```
60
+ */
61
+ abstract save(buffer: Buffer, collectorName: string, extension?: string): Promise<string>;
62
+ /**
63
+ * Retrieves previously stored binary data.
64
+ *
65
+ * Uses the identifier returned by save() to fetch the original data.
66
+ *
67
+ * @abstract
68
+ * @param {string} path - Storage identifier from save() operation
69
+ * @returns {Promise<Buffer>} The original binary data
70
+ * @throws {Error} When file doesn't exist or retrieval fails
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const path = '/storage/weather-sensor/2024-01-15_14-30-00.json'
75
+ * const data = await storage.retrieve(path)
76
+ * const json = JSON.parse(data.toString())
77
+ * ```
78
+ */
79
+ abstract retrieve(path: string): Promise<Buffer>;
80
+ /**
81
+ * Removes stored data permanently.
82
+ *
83
+ * Deletes the data associated with the given storage identifier.
84
+ *
85
+ * @abstract
86
+ * @param {string} path - Storage identifier from save() operation
87
+ * @returns {Promise<void>}
88
+ * @throws {Error} When deletion fails or path doesn't exist
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const path = '/storage/weather-sensor/old-data.json'
93
+ * await storage.delete(path)
94
+ * ```
95
+ */
96
+ abstract delete(path: string): Promise<void>;
97
+ /**
98
+ * Persists binary data at a specific path (no auto-generated filename).
99
+ *
100
+ * Unlike save(), this method stores the file at the exact path specified,
101
+ * preserving the original filename and directory structure.
102
+ * Useful for extracting archives where file paths must be preserved.
103
+ *
104
+ * @param {Buffer} buffer - Binary data to store
105
+ * @param {string} relativePath - Full relative path including filename (e.g., 'tilesets/123/tileset.json')
106
+ * @returns {Promise<string>} The same path that was provided (for consistency)
107
+ * @throws {Error} When storage operation fails
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * const buffer = Buffer.from('{"asset": {"version": "1.0"}}')
112
+ * const path = await storage.saveWithPath(buffer, 'tilesets/123/tileset.json')
113
+ * // Returns: 'tilesets/123/tileset.json'
114
+ * ```
115
+ */
116
+ abstract saveWithPath(buffer: Buffer, relativePath: string): Promise<string>;
117
+ /**
118
+ * Deletes multiple files in batch for better performance.
119
+ *
120
+ * Default implementation calls delete() sequentially, but storage backends
121
+ * can override this with optimized bulk delete operations (e.g., S3 DeleteObjects).
122
+ *
123
+ * @param {string[]} paths - Array of storage identifiers to delete
124
+ * @returns {Promise<void>}
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * await storage.deleteBatch([
129
+ * 'tilesets/123/tileset.json',
130
+ * 'tilesets/123/tile_0.b3dm',
131
+ * 'tilesets/123/tile_1.b3dm'
132
+ * ])
133
+ * ```
134
+ */
135
+ deleteBatch(paths: string[]): Promise<void>;
136
+ /**
137
+ * Returns the public URL for a stored file.
138
+ *
139
+ * For cloud storage (S3, OVH, Azure), this returns the direct HTTP URL.
140
+ * For local storage, this may return a relative path or throw an error.
141
+ *
142
+ * @abstract
143
+ * @param {string} relativePath - The storage path/key of the file
144
+ * @returns {string} The public URL to access the file directly
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const url = storage.getPublicUrl('tilesets/123/tileset.json')
149
+ * // Returns: 'https://bucket.s3.region.cloud.ovh.net/tilesets/123/tileset.json'
150
+ * ```
151
+ */
152
+ abstract getPublicUrl(relativePath: string): string;
153
+ /**
154
+ * Deletes all files under a given prefix/folder.
155
+ *
156
+ * This is more efficient than deleteBatch() when you don't know all file paths,
157
+ * as it lists objects by prefix and deletes them in bulk.
158
+ * Useful for deleting entire tilesets or component data.
159
+ *
160
+ * @abstract
161
+ * @param {string} prefix - The folder/prefix to delete (e.g., 'tilesets/123')
162
+ * @returns {Promise<number>} Number of files deleted
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * const count = await storage.deleteByPrefix('tilesets/123')
167
+ * // Deletes all files starting with 'tilesets/123/'
168
+ * console.log(`Deleted ${count} files`)
169
+ * ```
170
+ */
171
+ abstract deleteByPrefix(prefix: string): Promise<number>;
172
+ /**
173
+ * Whether this storage backend supports presigned URLs for direct uploads.
174
+ * Override in subclasses that support presigned URLs (e.g., S3-compatible storage).
175
+ */
176
+ supportsPresignedUrls(): boolean;
177
+ /**
178
+ * Generate a presigned PUT URL for direct client-to-storage uploads.
179
+ * Override in subclasses that support presigned URLs.
180
+ *
181
+ * @param key - The object key (path) in storage
182
+ * @param contentType - MIME type of the file to upload
183
+ * @param expiresInSeconds - URL validity duration (default: 300s = 5min)
184
+ * @returns Presigned upload URL, key, and expiration date
185
+ */
186
+ generatePresignedUploadUrl(_key: string, _contentType: string, _expiresInSeconds?: number): Promise<PresignedUploadResult>;
187
+ /**
188
+ * Check if an object exists in storage and return its metadata.
189
+ * Override in subclasses that support presigned URLs.
190
+ *
191
+ * @param key - The object key (path) in storage
192
+ * @returns Whether the object exists, with optional size and content type
193
+ */
194
+ objectExists(_key: string): Promise<ObjectExistsResult>;
195
+ }
196
+ //# sourceMappingURL=storage_service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage_service.d.ts","sourceRoot":"","sources":["../src/storage_service.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,8BAAsB,cAAc;IAChC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAEzF;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAEhD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAE5C;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE5E;;;;;;;;;;;;;;;;;OAiBG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAEnD;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAExD;;;OAGG;IACH,qBAAqB,IAAI,OAAO;IAIhC;;;;;;;;OAQG;IACG,0BAA0B,CAC5B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,iBAAiB,GAAE,MAAY,GAChC,OAAO,CAAC,qBAAqB,CAAC;IAIjC;;;;;;OAMG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAGhE"}
@@ -0,0 +1,86 @@
1
+ import { safeAsync, Logger } from '@cepseudo/shared';
2
+ const logger = new Logger('StorageService');
3
+ /**
4
+ * Abstract base class for storage service implementations.
5
+ *
6
+ * Defines the contract for persisting and retrieving binary data in the Digital Twin framework.
7
+ * Concrete implementations provide storage backends like local filesystem, AWS S3, Azure Blob, etc.
8
+ *
9
+ * @abstract
10
+ * @class StorageService
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Implement for specific storage backend
15
+ * class S3StorageService extends StorageService {
16
+ * async save(buffer: Buffer, collectorName: string, extension?: string): Promise<string> {
17
+ * // Upload to S3 bucket
18
+ * return 's3://bucket/path/to/file'
19
+ * }
20
+ *
21
+ * async retrieve(path: string): Promise<Buffer> {
22
+ * // Download from S3
23
+ * return buffer
24
+ * }
25
+ *
26
+ * async delete(path: string): Promise<void> {
27
+ * // Delete from S3
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export class StorageService {
33
+ /**
34
+ * Deletes multiple files in batch for better performance.
35
+ *
36
+ * Default implementation calls delete() sequentially, but storage backends
37
+ * can override this with optimized bulk delete operations (e.g., S3 DeleteObjects).
38
+ *
39
+ * @param {string[]} paths - Array of storage identifiers to delete
40
+ * @returns {Promise<void>}
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * await storage.deleteBatch([
45
+ * 'tilesets/123/tileset.json',
46
+ * 'tilesets/123/tile_0.b3dm',
47
+ * 'tilesets/123/tile_1.b3dm'
48
+ * ])
49
+ * ```
50
+ */
51
+ async deleteBatch(paths) {
52
+ // Default parallel implementation - subclasses can override with bulk operations
53
+ // Individual failures are logged but don't prevent other deletions
54
+ await Promise.all(paths.map(path => safeAsync(() => this.delete(path), `delete file ${path}`, logger)));
55
+ }
56
+ /**
57
+ * Whether this storage backend supports presigned URLs for direct uploads.
58
+ * Override in subclasses that support presigned URLs (e.g., S3-compatible storage).
59
+ */
60
+ supportsPresignedUrls() {
61
+ return false;
62
+ }
63
+ /**
64
+ * Generate a presigned PUT URL for direct client-to-storage uploads.
65
+ * Override in subclasses that support presigned URLs.
66
+ *
67
+ * @param key - The object key (path) in storage
68
+ * @param contentType - MIME type of the file to upload
69
+ * @param expiresInSeconds - URL validity duration (default: 300s = 5min)
70
+ * @returns Presigned upload URL, key, and expiration date
71
+ */
72
+ async generatePresignedUploadUrl(_key, _contentType, _expiresInSeconds = 300) {
73
+ throw new Error('Presigned URLs are not supported by this storage backend');
74
+ }
75
+ /**
76
+ * Check if an object exists in storage and return its metadata.
77
+ * Override in subclasses that support presigned URLs.
78
+ *
79
+ * @param key - The object key (path) in storage
80
+ * @returns Whether the object exists, with optional size and content type
81
+ */
82
+ async objectExists(_key) {
83
+ throw new Error('Object existence check is not supported by this storage backend');
84
+ }
85
+ }
86
+ //# sourceMappingURL=storage_service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage_service.js","sourceRoot":"","sources":["../src/storage_service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEpD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,gBAAgB,CAAC,CAAA;AAc3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,OAAgB,cAAc;IAiFhC;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,WAAW,CAAC,KAAe;QAC7B,iFAAiF;QACjF,mEAAmE;QACnE,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,eAAe,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAC3G,CAAC;IAwCD;;;OAGG;IACH,qBAAqB;QACjB,OAAO,KAAK,CAAA;IAChB,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,0BAA0B,CAC5B,IAAY,EACZ,YAAoB,EACpB,oBAA4B,GAAG;QAE/B,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;IAC/E,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAAC,IAAY;QAC3B,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAA;IACtF,CAAC;CACJ"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@cepseudo/storage",
3
+ "version": "1.0.0",
4
+ "description": "Storage service abstraction for Digital Twin framework (local filesystem, OVH S3)",
5
+ "license": "MIT",
6
+ "author": "Axel Hoffmann",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsc --watch",
24
+ "clean": "rimraf dist tsconfig.tsbuildinfo",
25
+ "test": "node -r ./bin/set-test-env.cjs --import ts-node-maintained/register/esm --enable-source-maps bin/test.ts",
26
+ "lint": "eslint src tests --no-error-on-unmatched-pattern",
27
+ "lint:fix": "eslint src tests --fix --no-error-on-unmatched-pattern"
28
+ },
29
+ "engines": {
30
+ "node": ">=20.0.0"
31
+ },
32
+ "dependencies": {
33
+ "@cepseudo/shared": "workspace:*"
34
+ },
35
+ "peerDependencies": {
36
+ "@aws-sdk/client-s3": ">=3.0.0",
37
+ "@aws-sdk/s3-request-presigner": ">=3.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@aws-sdk/client-s3": {
41
+ "optional": true
42
+ },
43
+ "@aws-sdk/s3-request-presigner": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@aws-sdk/client-s3": "^3.1002.0",
49
+ "@aws-sdk/s3-request-presigner": "^3.1002.0",
50
+ "@japa/assert": "^4.1.0",
51
+ "@japa/runner": "^4.3.0",
52
+ "testcontainers": "^10.0.0",
53
+ "ts-node-maintained": "^10.9.5"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/CePseudoBE/digitaltwin.git",
58
+ "directory": "packages/storage"
59
+ },
60
+ "keywords": [
61
+ "digitaltwin",
62
+ "storage",
63
+ "s3",
64
+ "ovh",
65
+ "presigned-url"
66
+ ],
67
+ "bugs": {
68
+ "url": "https://github.com/CePseudoBE/digitaltwin/issues"
69
+ },
70
+ "homepage": "https://github.com/CePseudoBE/digitaltwin#readme",
71
+ "publishConfig": {
72
+ "access": "public"
73
+ }
74
+ }