@objectstack/service-storage 4.0.4 → 4.1.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/README.md +108 -401
- package/dist/index.cjs +1343 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7645 -22
- package/dist/index.d.ts +7645 -22
- package/dist/index.js +1335 -66
- package/dist/index.js.map +1 -1
- package/package.json +43 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -177
- package/src/index.ts +0 -8
- package/src/local-storage-adapter.test.ts +0 -91
- package/src/local-storage-adapter.ts +0 -100
- package/src/s3-storage-adapter.ts +0 -88
- package/src/storage-service-plugin.ts +0 -66
- package/tsconfig.json +0 -17
package/README.md
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
# @objectstack/service-storage
|
|
2
2
|
|
|
3
|
-
Storage Service for ObjectStack — implements `IStorageService` with local filesystem and S3
|
|
3
|
+
Storage Service for ObjectStack — implements `IStorageService` with local filesystem and S3-compatible adapters, REST routes for front-end uploads, and presigned URL support.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Multiple Adapters**: Local filesystem (development) and S3-compatible storage (production)
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **Multipart Upload**: Support for large file uploads
|
|
14
|
-
- **Type-Safe**: Full TypeScript support
|
|
8
|
+
- **Presigned Uploads**: Browser-direct upload via presigned URLs (S3 native, local HMAC-signed tokens)
|
|
9
|
+
- **Chunked / Multipart Upload**: Resumable large file uploads with progress tracking
|
|
10
|
+
- **File Metadata Store**: `sys_file` object tracks fileId → key mapping and lifecycle status
|
|
11
|
+
- **REST Routes**: Auto-mounted `/api/v1/storage/*` endpoints consumed by `@objectstack/client`
|
|
12
|
+
- **Type-Safe**: Full TypeScript support with Zod-validated API contracts
|
|
15
13
|
|
|
16
14
|
## Installation
|
|
17
15
|
|
|
@@ -19,7 +17,7 @@ Storage Service for ObjectStack — implements `IStorageService` with local file
|
|
|
19
17
|
pnpm add @objectstack/service-storage
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
For S3 adapter:
|
|
20
|
+
For S3 adapter (optional peer dependencies):
|
|
23
21
|
```bash
|
|
24
22
|
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
25
23
|
```
|
|
@@ -27,17 +25,19 @@ pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
|
27
25
|
## Basic Usage
|
|
28
26
|
|
|
29
27
|
```typescript
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
import { ObjectKernel } from '@objectstack/core';
|
|
29
|
+
import { StorageServicePlugin } from '@objectstack/service-storage';
|
|
30
|
+
|
|
31
|
+
const kernel = new ObjectKernel();
|
|
32
|
+
kernel.use(new StorageServicePlugin({
|
|
33
|
+
adapter: 'local',
|
|
34
|
+
local: { rootDir: './uploads' },
|
|
35
|
+
}));
|
|
36
|
+
await kernel.bootstrap();
|
|
37
|
+
|
|
38
|
+
// Programmatic access
|
|
39
|
+
const storage = kernel.getService('file-storage');
|
|
40
|
+
await storage.upload('files/hello.txt', Buffer.from('hello'));
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## Configuration
|
|
@@ -45,422 +45,129 @@ const stack = defineStack({
|
|
|
45
45
|
### Local Filesystem Adapter (Development)
|
|
46
46
|
|
|
47
47
|
```typescript
|
|
48
|
-
|
|
48
|
+
new StorageServicePlugin({
|
|
49
49
|
adapter: 'local',
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
local: {
|
|
51
|
+
rootDir: './uploads',
|
|
52
|
+
baseUrl: 'http://localhost:3000', // for presigned URLs
|
|
53
|
+
signingSecret: 'dev-secret', // auto-generated if omitted
|
|
54
|
+
},
|
|
55
|
+
presignedTtl: 3600, // 1 hour
|
|
56
|
+
sessionTtl: 86400, // 24 hours for chunked uploads
|
|
52
57
|
});
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
### S3 Adapter (Production)
|
|
56
61
|
|
|
57
62
|
```typescript
|
|
58
|
-
|
|
63
|
+
new StorageServicePlugin({
|
|
59
64
|
adapter: 's3',
|
|
60
65
|
s3: {
|
|
61
66
|
bucket: 'my-bucket',
|
|
62
67
|
region: 'us-east-1',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
68
|
+
// Optional for S3-compatible services (R2, MinIO, Spaces):
|
|
69
|
+
// endpoint: 'https://r2.cloudflarestorage.com/account-id',
|
|
70
|
+
// forcePathStyle: true,
|
|
67
71
|
},
|
|
68
|
-
baseUrl: 'https://my-bucket.s3.amazonaws.com',
|
|
69
|
-
});
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### S3-Compatible Services (Cloudflare R2, DigitalOcean Spaces, MinIO)
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
ServiceStorage.configure({
|
|
76
|
-
adapter: 's3',
|
|
77
|
-
s3: {
|
|
78
|
-
bucket: 'my-bucket',
|
|
79
|
-
region: 'auto',
|
|
80
|
-
endpoint: 'https://r2.cloudflarestorage.com/account-id',
|
|
81
|
-
credentials: {
|
|
82
|
-
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
83
|
-
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Service API
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
// Get storage service
|
|
93
|
-
const storage = kernel.getService<IStorageService>('storage');
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### File Upload
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
// Upload a file from buffer
|
|
100
|
-
await storage.upload({
|
|
101
|
-
path: 'documents/contract.pdf',
|
|
102
|
-
data: fileBuffer,
|
|
103
|
-
contentType: 'application/pdf',
|
|
104
|
-
metadata: {
|
|
105
|
-
userId: 'user:123',
|
|
106
|
-
category: 'contracts',
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Upload from stream
|
|
111
|
-
await storage.uploadStream({
|
|
112
|
-
path: 'videos/demo.mp4',
|
|
113
|
-
stream: fileStream,
|
|
114
|
-
contentType: 'video/mp4',
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Upload with automatic path generation
|
|
118
|
-
const path = await storage.uploadAuto({
|
|
119
|
-
data: fileBuffer,
|
|
120
|
-
fileName: 'profile.jpg',
|
|
121
|
-
folder: 'avatars',
|
|
122
|
-
contentType: 'image/jpeg',
|
|
123
72
|
});
|
|
124
|
-
// Returns: 'avatars/2024/01/15/abc123-profile.jpg'
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### File Download
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
// Download file as buffer
|
|
131
|
-
const file = await storage.download('documents/contract.pdf');
|
|
132
|
-
console.log(file.data); // Buffer
|
|
133
|
-
console.log(file.contentType); // 'application/pdf'
|
|
134
|
-
console.log(file.size); // File size in bytes
|
|
135
|
-
|
|
136
|
-
// Download as stream
|
|
137
|
-
const stream = await storage.downloadStream('videos/demo.mp4');
|
|
138
|
-
stream.pipe(res); // Pipe to HTTP response
|
|
139
73
|
```
|
|
140
74
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
// Check if file exists
|
|
145
|
-
const exists = await storage.exists('documents/contract.pdf');
|
|
146
|
-
|
|
147
|
-
// Get file metadata
|
|
148
|
-
const metadata = await storage.getMetadata('documents/contract.pdf');
|
|
149
|
-
// {
|
|
150
|
-
// size: 1024000,
|
|
151
|
-
// contentType: 'application/pdf',
|
|
152
|
-
// lastModified: Date,
|
|
153
|
-
// metadata: { userId: 'user:123', category: 'contracts' }
|
|
154
|
-
// }
|
|
155
|
-
|
|
156
|
-
// Delete file
|
|
157
|
-
await storage.delete('documents/contract.pdf');
|
|
158
|
-
|
|
159
|
-
// Copy file
|
|
160
|
-
await storage.copy({
|
|
161
|
-
from: 'documents/contract.pdf',
|
|
162
|
-
to: 'archive/2024/contract.pdf',
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Move file
|
|
166
|
-
await storage.move({
|
|
167
|
-
from: 'temp/upload.pdf',
|
|
168
|
-
to: 'documents/contract.pdf',
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Directory Operations
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
// List files in directory
|
|
176
|
-
const files = await storage.list('documents', {
|
|
177
|
-
recursive: false,
|
|
178
|
-
limit: 100,
|
|
179
|
-
});
|
|
180
|
-
// Returns: ['contract.pdf', 'invoice.pdf', 'report.docx']
|
|
181
|
-
|
|
182
|
-
// List with metadata
|
|
183
|
-
const files = await storage.listDetailed('documents');
|
|
184
|
-
// Returns: [
|
|
185
|
-
// { path: 'contract.pdf', size: 1024000, lastModified: Date },
|
|
186
|
-
// { path: 'invoice.pdf', size: 512000, lastModified: Date },
|
|
187
|
-
// ]
|
|
188
|
-
|
|
189
|
-
// Delete directory and all contents
|
|
190
|
-
await storage.deleteDirectory('temp');
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### URL Generation
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
// Generate public URL (for public files)
|
|
197
|
-
const url = storage.getUrl('public/logo.png');
|
|
198
|
-
// 'https://my-bucket.s3.amazonaws.com/public/logo.png'
|
|
199
|
-
|
|
200
|
-
// Generate signed URL (for private files, expires in 1 hour)
|
|
201
|
-
const signedUrl = await storage.getSignedUrl('documents/contract.pdf', {
|
|
202
|
-
expiresIn: 3600,
|
|
203
|
-
operation: 'read', // or 'write'
|
|
204
|
-
});
|
|
205
|
-
// 'https://my-bucket.s3.amazonaws.com/documents/contract.pdf?X-Amz-Signature=...'
|
|
206
|
-
|
|
207
|
-
// Generate upload URL (for direct client uploads)
|
|
208
|
-
const uploadUrl = await storage.getUploadUrl('uploads/temp.pdf', {
|
|
209
|
-
expiresIn: 900, // 15 minutes
|
|
210
|
-
contentType: 'application/pdf',
|
|
211
|
-
maxSize: 10485760, // 10MB
|
|
212
|
-
});
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
## Advanced Features
|
|
216
|
-
|
|
217
|
-
### Multipart Upload (Large Files)
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
// Initialize multipart upload
|
|
221
|
-
const uploadId = await storage.initMultipartUpload({
|
|
222
|
-
path: 'large-files/video.mp4',
|
|
223
|
-
contentType: 'video/mp4',
|
|
224
|
-
});
|
|
75
|
+
## REST API Endpoints
|
|
225
76
|
|
|
226
|
-
|
|
227
|
-
const parts = [];
|
|
228
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
229
|
-
const part = await storage.uploadPart({
|
|
230
|
-
uploadId,
|
|
231
|
-
partNumber: i + 1,
|
|
232
|
-
data: chunks[i],
|
|
233
|
-
});
|
|
234
|
-
parts.push(part);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Complete multipart upload
|
|
238
|
-
await storage.completeMultipartUpload({
|
|
239
|
-
uploadId,
|
|
240
|
-
parts,
|
|
241
|
-
});
|
|
77
|
+
All routes are mounted at `/api/v1/storage` (configurable via `basePath`).
|
|
242
78
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
79
|
+
| Method | Path | Description |
|
|
80
|
+
|--------|------|-------------|
|
|
81
|
+
| POST | `/upload/presigned` | Get presigned upload URL |
|
|
82
|
+
| POST | `/upload/complete` | Mark upload as committed |
|
|
83
|
+
| POST | `/upload/chunked` | Initiate chunked upload |
|
|
84
|
+
| PUT | `/upload/chunked/:uploadId/chunk/:chunkIndex` | Upload a chunk |
|
|
85
|
+
| POST | `/upload/chunked/:uploadId/complete` | Complete chunked upload |
|
|
86
|
+
| GET | `/upload/chunked/:uploadId/progress` | Get upload progress |
|
|
87
|
+
| GET | `/files/:fileId/url` | Get download URL |
|
|
88
|
+
| PUT | `/_local/raw/:token` | Local raw upload (presigned) |
|
|
89
|
+
| GET | `/_local/raw/:token` | Local raw download (presigned) |
|
|
246
90
|
|
|
247
|
-
|
|
91
|
+
## Client SDK Usage
|
|
248
92
|
|
|
249
93
|
```typescript
|
|
250
|
-
|
|
251
|
-
const presignedPost = await storage.getPresignedPost({
|
|
252
|
-
path: 'uploads/${filename}',
|
|
253
|
-
conditions: [
|
|
254
|
-
['content-length-range', 0, 10485760], // Max 10MB
|
|
255
|
-
['starts-with', '$Content-Type', 'image/'], // Only images
|
|
256
|
-
],
|
|
257
|
-
expiresIn: 900, // 15 minutes
|
|
258
|
-
});
|
|
94
|
+
import { ObjectStackClient } from '@objectstack/client';
|
|
259
95
|
|
|
260
|
-
|
|
261
|
-
const formData = new FormData();
|
|
262
|
-
Object.entries(presignedPost.fields).forEach(([key, value]) => {
|
|
263
|
-
formData.append(key, value);
|
|
264
|
-
});
|
|
265
|
-
formData.append('file', file);
|
|
96
|
+
const client = new ObjectStackClient({ baseUrl: 'http://localhost:3000' });
|
|
266
97
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
body: formData,
|
|
270
|
-
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### Image Processing Integration
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
// Upload original image
|
|
277
|
-
await storage.upload({
|
|
278
|
-
path: 'images/original/photo.jpg',
|
|
279
|
-
data: imageBuffer,
|
|
280
|
-
contentType: 'image/jpeg',
|
|
281
|
-
});
|
|
98
|
+
// Simple upload (presigned URL flow)
|
|
99
|
+
const result = await client.storage.upload(file, 'user');
|
|
282
100
|
|
|
283
|
-
//
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
contentType: 'image/jpeg',
|
|
101
|
+
// Chunked upload for large files
|
|
102
|
+
const session = await client.storage.initChunkedUpload({
|
|
103
|
+
filename: 'large-video.mp4',
|
|
104
|
+
mimeType: 'video/mp4',
|
|
105
|
+
totalSize: file.size,
|
|
289
106
|
});
|
|
290
107
|
|
|
291
|
-
|
|
292
|
-
await storage.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
108
|
+
// Resume interrupted upload
|
|
109
|
+
const completed = await client.storage.resumeUpload(
|
|
110
|
+
session.data.uploadId,
|
|
111
|
+
file,
|
|
112
|
+
session.data.chunkSize,
|
|
113
|
+
session.data.resumeToken,
|
|
114
|
+
);
|
|
297
115
|
```
|
|
298
116
|
|
|
299
|
-
|
|
117
|
+
## Architecture
|
|
300
118
|
|
|
301
|
-
```typescript
|
|
302
|
-
// Attach file to a record
|
|
303
|
-
await storage.upload({
|
|
304
|
-
path: `attachments/opportunity/${opportunityId}/proposal.pdf`,
|
|
305
|
-
data: fileBuffer,
|
|
306
|
-
contentType: 'application/pdf',
|
|
307
|
-
metadata: {
|
|
308
|
-
objectType: 'opportunity',
|
|
309
|
-
recordId: opportunityId,
|
|
310
|
-
uploadedBy: 'user:123',
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// List attachments for a record
|
|
315
|
-
const attachments = await storage.list(`attachments/opportunity/${opportunityId}`);
|
|
316
|
-
|
|
317
|
-
// Delete all attachments when record is deleted
|
|
318
|
-
await storage.deleteDirectory(`attachments/opportunity/${opportunityId}`);
|
|
319
119
|
```
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
GET /api/v1/storage/metadata/:path # Get file metadata
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
## Client Integration
|
|
334
|
-
|
|
335
|
-
### React Component Example
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
import { useStorage } from '@objectstack/client-react';
|
|
339
|
-
|
|
340
|
-
function FileUploader() {
|
|
341
|
-
const { upload, uploading, progress } = useStorage();
|
|
342
|
-
|
|
343
|
-
const handleUpload = async (file: File) => {
|
|
344
|
-
const path = await upload({
|
|
345
|
-
file,
|
|
346
|
-
folder: 'documents',
|
|
347
|
-
onProgress: (percent) => console.log(`Upload: ${percent}%`),
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
console.log('Uploaded to:', path);
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
return (
|
|
354
|
-
<div>
|
|
355
|
-
<input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
|
|
356
|
-
{uploading && <progress value={progress} max="100" />}
|
|
357
|
-
</div>
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
## Common Patterns
|
|
363
|
-
|
|
364
|
-
### User Avatar Upload
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
async function uploadAvatar(userId: string, imageFile: Buffer) {
|
|
368
|
-
// Upload original
|
|
369
|
-
const path = `avatars/${userId}/original.jpg`;
|
|
370
|
-
await storage.upload({
|
|
371
|
-
path,
|
|
372
|
-
data: imageFile,
|
|
373
|
-
contentType: 'image/jpeg',
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
// Generate thumbnail
|
|
377
|
-
const thumbnail = await resizeImage(imageFile, { width: 128, height: 128 });
|
|
378
|
-
await storage.upload({
|
|
379
|
-
path: `avatars/${userId}/thumbnail.jpg`,
|
|
380
|
-
data: thumbnail,
|
|
381
|
-
contentType: 'image/jpeg',
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
return {
|
|
385
|
-
original: storage.getUrl(path),
|
|
386
|
-
thumbnail: storage.getUrl(`avatars/${userId}/thumbnail.jpg`),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Document Management
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
async function uploadDocument(doc: {
|
|
395
|
-
recordId: string;
|
|
396
|
-
file: Buffer;
|
|
397
|
-
fileName: string;
|
|
398
|
-
uploadedBy: string;
|
|
399
|
-
}) {
|
|
400
|
-
const path = `documents/${doc.recordId}/${Date.now()}-${doc.fileName}`;
|
|
401
|
-
|
|
402
|
-
await storage.upload({
|
|
403
|
-
path,
|
|
404
|
-
data: doc.file,
|
|
405
|
-
contentType: getMimeType(doc.fileName),
|
|
406
|
-
metadata: {
|
|
407
|
-
recordId: doc.recordId,
|
|
408
|
-
uploadedBy: doc.uploadedBy,
|
|
409
|
-
fileName: doc.fileName,
|
|
410
|
-
},
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Create signed URL for secure download
|
|
414
|
-
const downloadUrl = await storage.getSignedUrl(path, { expiresIn: 86400 }); // 24 hours
|
|
415
|
-
|
|
416
|
-
return { path, downloadUrl };
|
|
417
|
-
}
|
|
120
|
+
┌──────────────┐ ┌─────────────────────┐ ┌──────────────────┐
|
|
121
|
+
│ Client SDK │────▶│ REST Routes │────▶│ IStorageService │
|
|
122
|
+
│ (browser) │ │ /api/v1/storage/* │ │ (adapter) │
|
|
123
|
+
└──────────────┘ └─────────────────────┘ └──────────────────┘
|
|
124
|
+
│ │
|
|
125
|
+
▼ ▼
|
|
126
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
127
|
+
│ MetadataStore │ │ Filesystem / S3 │
|
|
128
|
+
│ (sys_file) │ │ (actual bytes) │
|
|
129
|
+
└─────────────────┘ └─────────────────┘
|
|
418
130
|
```
|
|
419
131
|
|
|
420
|
-
##
|
|
421
|
-
|
|
422
|
-
1. **Path Organization**: Use hierarchical paths (e.g., `object/recordId/filename`)
|
|
423
|
-
2. **Content Types**: Always specify correct `contentType`
|
|
424
|
-
3. **Security**: Use signed URLs for private files
|
|
425
|
-
4. **Cleanup**: Delete files when records are deleted
|
|
426
|
-
5. **Validation**: Validate file types and sizes before upload
|
|
427
|
-
6. **Metadata**: Store useful metadata with files
|
|
428
|
-
7. **Backups**: Implement backup strategy for S3 buckets
|
|
132
|
+
## System Objects
|
|
429
133
|
|
|
430
|
-
|
|
134
|
+
The plugin registers two system objects via the manifest service:
|
|
431
135
|
|
|
432
|
-
-
|
|
433
|
-
-
|
|
434
|
-
- **Compression**: Compress files before upload when appropriate
|
|
435
|
-
- **Caching**: Cache file URLs and metadata
|
|
436
|
-
- **Multipart**: Use multipart upload for files > 5MB
|
|
437
|
-
|
|
438
|
-
## Contract Implementation
|
|
439
|
-
|
|
440
|
-
Implements `IStorageService` from `@objectstack/spec/contracts`:
|
|
441
|
-
|
|
442
|
-
```typescript
|
|
443
|
-
interface IStorageService {
|
|
444
|
-
upload(options: UploadOptions): Promise<void>;
|
|
445
|
-
uploadStream(options: UploadStreamOptions): Promise<void>;
|
|
446
|
-
download(path: string): Promise<FileData>;
|
|
447
|
-
downloadStream(path: string): Promise<ReadableStream>;
|
|
448
|
-
delete(path: string): Promise<void>;
|
|
449
|
-
exists(path: string): Promise<boolean>;
|
|
450
|
-
getMetadata(path: string): Promise<FileMetadata>;
|
|
451
|
-
list(path: string, options?: ListOptions): Promise<string[]>;
|
|
452
|
-
getUrl(path: string): string;
|
|
453
|
-
getSignedUrl(path: string, options?: SignedUrlOptions): Promise<string>;
|
|
454
|
-
}
|
|
455
|
-
```
|
|
136
|
+
- **`sys_file`** — File metadata (fileId, key, name, mimeType, size, scope, status)
|
|
137
|
+
- **`sys_upload_session`** — Chunked upload state (progress, parts, resumeToken)
|
|
456
138
|
|
|
457
139
|
## License
|
|
458
140
|
|
|
459
141
|
Apache-2.0
|
|
460
142
|
|
|
461
|
-
##
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
-
|
|
466
|
-
|
|
143
|
+
## UI-driven configuration
|
|
144
|
+
|
|
145
|
+
`StorageServicePlugin` registers a `storage` Settings namespace (mail-style)
|
|
146
|
+
so administrators can switch adapter, configure S3 credentials, and tune
|
|
147
|
+
TTL / max-upload limits from the Settings hub instead of restarting the
|
|
148
|
+
process.
|
|
149
|
+
|
|
150
|
+
- Service key in the kernel: `file-storage` — registered as a
|
|
151
|
+
`SwappableStorageService` proxy at `init` time. The inner adapter
|
|
152
|
+
(local FS or S3) is rebuilt and swapped in on every `settings:changed`
|
|
153
|
+
event for `namespace=storage`.
|
|
154
|
+
- The S3 secret key is stored encrypted in `sys_secret` via the
|
|
155
|
+
CryptoAdapter / KMS chain set up by `service-settings`.
|
|
156
|
+
- A `storage/test` action handler uploads → downloads → deletes a small
|
|
157
|
+
probe blob to validate the configuration end-to-end. The handler is
|
|
158
|
+
registered on `kernel:ready`; the `service-settings` package ships a
|
|
159
|
+
validation-only fallback for kernels that mount Settings but not
|
|
160
|
+
Storage.
|
|
161
|
+
|
|
162
|
+
### ⚠ Switching adapters does not migrate files
|
|
163
|
+
|
|
164
|
+
Files uploaded under the previous adapter remain on that backend and
|
|
165
|
+
become unreachable through the new one. The plugin logs a warning on
|
|
166
|
+
every swap. Migrate data out-of-band (e.g. `aws s3 sync` from the local
|
|
167
|
+
root to the new bucket) before flipping the toggle in production.
|
|
168
|
+
|
|
169
|
+
### Disabling the live-wire
|
|
170
|
+
|
|
171
|
+
Pass `bindToSettings: false` to keep the constructor-supplied adapter
|
|
172
|
+
frozen — useful in tests and in deployments where storage config must
|
|
173
|
+
come from env vars only.
|