@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 +21 -0
- package/README.md +198 -0
- package/dist/adapters/local_storage_service.d.ts +57 -0
- package/dist/adapters/local_storage_service.d.ts.map +1 -0
- package/dist/adapters/local_storage_service.js +132 -0
- package/dist/adapters/local_storage_service.js.map +1 -0
- package/dist/adapters/ovh_storage_service.d.ts +86 -0
- package/dist/adapters/ovh_storage_service.d.ts.map +1 -0
- package/dist/adapters/ovh_storage_service.js +247 -0
- package/dist/adapters/ovh_storage_service.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/storage_factory.d.ts +14 -0
- package/dist/storage_factory.d.ts.map +1 -0
- package/dist/storage_factory.js +41 -0
- package/dist/storage_factory.js.map +1 -0
- package/dist/storage_service.d.ts +196 -0
- package/dist/storage_service.d.ts.map +1 -0
- package/dist/storage_service.js +86 -0
- package/dist/storage_service.js.map +1 -0
- package/package.json +74 -0
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
|
+
[](https://www.npmjs.com/package/@cepseudo/storage)
|
|
4
|
+
[](../../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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|