@owlmeans/storage-resource 0.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/LICENSE +21 -0
- package/README.md +512 -0
- package/build/consts.d.ts +3 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +4 -0
- package/build/consts.js.map +1 -0
- package/build/helper.d.ts +3 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +13 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/model.d.ts +1 -0
- package/build/model.d.ts.map +1 -0
- package/build/model.js +2 -0
- package/build/model.js.map +1 -0
- package/build/resource.d.ts +4 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +72 -0
- package/build/resource.js.map +1 -0
- package/build/types.d.ts +31 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +43 -0
- package/src/consts.ts +5 -0
- package/src/helper.ts +22 -0
- package/src/index.ts +5 -0
- package/src/model.ts +0 -0
- package/src/resource.ts +93 -0
- package/src/types.ts +31 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
|
|
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,512 @@
|
|
|
1
|
+
# @owlmeans/storage-resource
|
|
2
|
+
|
|
3
|
+
Object storage resource integration for OwlMeans Common applications. This package provides S3-compatible object storage functionality with file upload, download, type validation, and stream processing capabilities designed for secure file management in distributed applications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@owlmeans/storage-resource` package extends the OwlMeans resource system to provide object storage functionality including:
|
|
8
|
+
|
|
9
|
+
- **S3-Compatible Storage**: Integration with AWS S3 and S3-compatible storage services
|
|
10
|
+
- **File Upload/Download**: Secure file operations with validation and type checking
|
|
11
|
+
- **Stream Processing**: Efficient handling of file streams for large files
|
|
12
|
+
- **Type Validation**: Automatic file type detection and validation
|
|
13
|
+
- **Prefix Management**: Organized file storage with configurable prefixes
|
|
14
|
+
- **Resource Integration**: Seamless integration with OwlMeans resource management system
|
|
15
|
+
|
|
16
|
+
This package follows the OwlMeans resource pattern and provides server-side storage capabilities for applications requiring file management.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @owlmeans/storage-resource
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Dependencies
|
|
25
|
+
|
|
26
|
+
This package requires and integrates with:
|
|
27
|
+
- `@owlmeans/resource`: Base resource management system
|
|
28
|
+
- `@owlmeans/server-context`: Server context management
|
|
29
|
+
- `@owlmeans/storage-common`: Storage utilities and error types
|
|
30
|
+
- `@aws-sdk/client-s3`: AWS S3 SDK for storage operations
|
|
31
|
+
- `file-type`: File type detection utilities
|
|
32
|
+
|
|
33
|
+
## Core Concepts
|
|
34
|
+
|
|
35
|
+
### Storage Resource
|
|
36
|
+
|
|
37
|
+
A specialized resource that manages file storage operations including upload, download, and metadata management through S3-compatible APIs.
|
|
38
|
+
|
|
39
|
+
### File Validation
|
|
40
|
+
|
|
41
|
+
Built-in file type validation ensures that uploaded files match their declared MIME types, providing security against file type spoofing.
|
|
42
|
+
|
|
43
|
+
### Stream Processing
|
|
44
|
+
|
|
45
|
+
Efficient handling of file streams allows for processing large files without excessive memory usage.
|
|
46
|
+
|
|
47
|
+
### Bucket Configuration
|
|
48
|
+
|
|
49
|
+
Support for multiple storage buckets with different configurations for organizing files by purpose or security level.
|
|
50
|
+
|
|
51
|
+
## API Reference
|
|
52
|
+
|
|
53
|
+
### Types
|
|
54
|
+
|
|
55
|
+
#### `StoredRecord`
|
|
56
|
+
Interface for stored file records extending ResourceRecord.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface StoredRecord extends ResourceRecord {
|
|
60
|
+
url?: string // File URL for access
|
|
61
|
+
size?: number // File size in bytes
|
|
62
|
+
prefix: string // Storage prefix/path
|
|
63
|
+
stream?: Readable // File stream for upload
|
|
64
|
+
format?: StoredFileFormat // File format information
|
|
65
|
+
type?: string // MIME type
|
|
66
|
+
bytes?: Uint8Array // File bytes (alternative to stream)
|
|
67
|
+
base64?: string // Base64 encoded file data
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### `StorageConfig`
|
|
72
|
+
Configuration for storage bucket access.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
interface StorageConfig {
|
|
76
|
+
url: string // Storage service URL
|
|
77
|
+
apiKey: string // API key in format "keyId:keySecret"
|
|
78
|
+
basePrefix: string // Base prefix for all files in this bucket
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### `StorageResource`
|
|
83
|
+
Resource interface for storage operations.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface StorageResource extends Resource<StoredRecord> {
|
|
87
|
+
// Inherits all Resource methods for CRUD operations
|
|
88
|
+
// Specialized for file storage with validation
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### `StoredConfigAppend`
|
|
93
|
+
Configuration extension for storage bucket definitions.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
interface StoredConfigAppend {
|
|
97
|
+
storageBuckets: { [key: string]: StorageConfig }
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Factory Functions
|
|
102
|
+
|
|
103
|
+
#### `createStorageResource(alias?: string, configKey?: string): StorageResource`
|
|
104
|
+
|
|
105
|
+
Creates a storage resource instance with S3-compatible operations.
|
|
106
|
+
|
|
107
|
+
**Parameters:**
|
|
108
|
+
- `alias` (optional): Resource alias (default: `DEFAULT_ALIAS`)
|
|
109
|
+
- `configKey` (optional): Configuration key for bucket selection (defaults to alias)
|
|
110
|
+
|
|
111
|
+
**Returns:** StorageResource instance
|
|
112
|
+
|
|
113
|
+
**Features:**
|
|
114
|
+
- File upload with type validation
|
|
115
|
+
- Stream processing for efficient memory usage
|
|
116
|
+
- S3-compatible storage operations
|
|
117
|
+
- Automatic file type detection
|
|
118
|
+
- Error handling for storage operations
|
|
119
|
+
|
|
120
|
+
**Example:**
|
|
121
|
+
```typescript
|
|
122
|
+
import { createStorageResource } from '@owlmeans/storage-resource'
|
|
123
|
+
|
|
124
|
+
const storageResource = createStorageResource('files', 'main-bucket')
|
|
125
|
+
|
|
126
|
+
// Use with context
|
|
127
|
+
context.registerResource(storageResource)
|
|
128
|
+
await context.configure().init()
|
|
129
|
+
|
|
130
|
+
// Upload file
|
|
131
|
+
const uploadedFile = await storageResource.create({
|
|
132
|
+
stream: fileStream,
|
|
133
|
+
size: fileSize,
|
|
134
|
+
type: 'image/jpeg',
|
|
135
|
+
prefix: 'uploads/images'
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Storage Operations
|
|
140
|
+
|
|
141
|
+
#### File Upload
|
|
142
|
+
|
|
143
|
+
Upload files with validation and type checking:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const fileRecord = {
|
|
147
|
+
stream: fileStream, // Readable stream
|
|
148
|
+
size: fileSize, // File size in bytes
|
|
149
|
+
type: 'image/jpeg', // Expected MIME type
|
|
150
|
+
prefix: 'uploads/avatars' // Storage prefix
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const storedFile = await storageResource.create(fileRecord)
|
|
155
|
+
console.log(`File uploaded: ${storedFile.url}`)
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof FileTypeError) {
|
|
158
|
+
// Handle file type mismatch
|
|
159
|
+
} else if (error instanceof FileStreamError) {
|
|
160
|
+
// Handle stream errors
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### File Retrieval
|
|
166
|
+
|
|
167
|
+
Retrieve file metadata and URLs:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Get file by ID
|
|
171
|
+
const file = await storageResource.get(fileId)
|
|
172
|
+
console.log(`File URL: ${file.url}`)
|
|
173
|
+
|
|
174
|
+
// Search files by prefix
|
|
175
|
+
const files = await storageResource.list({
|
|
176
|
+
filter: { prefix: 'uploads/images' }
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Error Handling
|
|
181
|
+
|
|
182
|
+
The package provides specific error types for storage operations:
|
|
183
|
+
|
|
184
|
+
#### `FileStreamError`
|
|
185
|
+
Thrown when file stream is missing or invalid.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
if (record.stream == null) {
|
|
189
|
+
throw new FileStreamError('no')
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### `FilePropertyError`
|
|
194
|
+
Thrown when required file properties are missing.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
if (record.size == null) {
|
|
198
|
+
throw new FilePropertyError('size')
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### `FileTypeError`
|
|
203
|
+
Thrown when file type doesn't match declared MIME type.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
if (type?.mime !== record.type) {
|
|
207
|
+
throw new FileTypeError('mime-mismatch')
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `StorageApiError`
|
|
212
|
+
Thrown when storage API operations fail.
|
|
213
|
+
|
|
214
|
+
### Constants
|
|
215
|
+
|
|
216
|
+
#### `DEFAULT_ALIAS`
|
|
217
|
+
Default alias for storage resources.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const DEFAULT_ALIAS = 'storage'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Usage Examples
|
|
224
|
+
|
|
225
|
+
### Basic Storage Setup
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { createStorageResource } from '@owlmeans/storage-resource'
|
|
229
|
+
import { makeServerContext } from '@owlmeans/server-context'
|
|
230
|
+
|
|
231
|
+
// Configure server context with storage buckets
|
|
232
|
+
const context = makeServerContext({
|
|
233
|
+
service: 'file-service',
|
|
234
|
+
type: AppType.Backend,
|
|
235
|
+
layer: Layer.Service,
|
|
236
|
+
storageBuckets: {
|
|
237
|
+
'main': {
|
|
238
|
+
url: 'my-bucket.s3.amazonaws.com',
|
|
239
|
+
apiKey: 'AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
240
|
+
basePrefix: 'files'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Create and register storage resource
|
|
246
|
+
const storageResource = createStorageResource('storage', 'main')
|
|
247
|
+
context.registerResource(storageResource)
|
|
248
|
+
|
|
249
|
+
await context.configure().init()
|
|
250
|
+
|
|
251
|
+
// Storage resource is now ready for use
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### File Upload with Validation
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { Readable } from 'stream'
|
|
258
|
+
|
|
259
|
+
const uploadFile = async (fileBuffer: Buffer, mimeType: string, filename: string) => {
|
|
260
|
+
const fileStream = Readable.from(fileBuffer)
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const uploadedFile = await storageResource.create({
|
|
264
|
+
stream: fileStream,
|
|
265
|
+
size: fileBuffer.length,
|
|
266
|
+
type: mimeType,
|
|
267
|
+
prefix: `uploads/${new Date().getFullYear()}`
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
id: uploadedFile.id,
|
|
272
|
+
url: uploadedFile.url,
|
|
273
|
+
size: uploadedFile.size
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error instanceof FileTypeError) {
|
|
277
|
+
throw new Error('File type mismatch - uploaded file doesn\'t match declared type')
|
|
278
|
+
} else if (error instanceof FilePropertyError) {
|
|
279
|
+
throw new Error('Missing required file properties')
|
|
280
|
+
} else {
|
|
281
|
+
throw new Error('File upload failed')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Multiple Storage Buckets
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const context = makeServerContext({
|
|
291
|
+
service: 'multi-storage-app',
|
|
292
|
+
storageBuckets: {
|
|
293
|
+
'public': {
|
|
294
|
+
url: 'public-bucket.s3.amazonaws.com',
|
|
295
|
+
apiKey: 'public-key:public-secret',
|
|
296
|
+
basePrefix: 'public'
|
|
297
|
+
},
|
|
298
|
+
'private': {
|
|
299
|
+
url: 'private-bucket.s3.amazonaws.com',
|
|
300
|
+
apiKey: 'private-key:private-secret',
|
|
301
|
+
basePrefix: 'private'
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Create resources for different buckets
|
|
307
|
+
const publicStorage = createStorageResource('public-storage', 'public')
|
|
308
|
+
const privateStorage = createStorageResource('private-storage', 'private')
|
|
309
|
+
|
|
310
|
+
context.registerResource(publicStorage)
|
|
311
|
+
context.registerResource(privateStorage)
|
|
312
|
+
|
|
313
|
+
await context.configure().init()
|
|
314
|
+
|
|
315
|
+
// Upload to public bucket
|
|
316
|
+
const publicFile = await publicStorage.create({
|
|
317
|
+
stream: publicFileStream,
|
|
318
|
+
size: fileSize,
|
|
319
|
+
type: 'image/jpeg',
|
|
320
|
+
prefix: 'avatars'
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
// Upload to private bucket
|
|
324
|
+
const privateFile = await privateStorage.create({
|
|
325
|
+
stream: privateFileStream,
|
|
326
|
+
size: fileSize,
|
|
327
|
+
type: 'application/pdf',
|
|
328
|
+
prefix: 'documents'
|
|
329
|
+
})
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### File Type Validation
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const uploadWithValidation = async (fileStream: Readable, declaredType: string, size: number) => {
|
|
336
|
+
try {
|
|
337
|
+
const result = await storageResource.create({
|
|
338
|
+
stream: fileStream,
|
|
339
|
+
size: size,
|
|
340
|
+
type: declaredType,
|
|
341
|
+
prefix: 'validated-uploads'
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
console.log('File upload successful with type validation')
|
|
345
|
+
return result
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (error instanceof FileTypeError) {
|
|
348
|
+
console.error('File type validation failed - file content doesn\'t match declared MIME type')
|
|
349
|
+
throw error
|
|
350
|
+
}
|
|
351
|
+
throw error
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Example usage
|
|
356
|
+
const jpegStream = fs.createReadStream('image.jpg')
|
|
357
|
+
await uploadWithValidation(jpegStream, 'image/jpeg', jpegStats.size)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Alternative Upload Methods
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// Upload from bytes
|
|
364
|
+
const uploadFromBytes = async (bytes: Uint8Array, type: string) => {
|
|
365
|
+
const stream = Readable.from(Buffer.from(bytes))
|
|
366
|
+
|
|
367
|
+
return await storageResource.create({
|
|
368
|
+
stream: stream,
|
|
369
|
+
size: bytes.length,
|
|
370
|
+
type: type,
|
|
371
|
+
prefix: 'byte-uploads'
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Upload from base64
|
|
376
|
+
const uploadFromBase64 = async (base64Data: string, type: string) => {
|
|
377
|
+
const buffer = Buffer.from(base64Data, 'base64')
|
|
378
|
+
const stream = Readable.from(buffer)
|
|
379
|
+
|
|
380
|
+
return await storageResource.create({
|
|
381
|
+
stream: stream,
|
|
382
|
+
size: buffer.length,
|
|
383
|
+
type: type,
|
|
384
|
+
prefix: 'base64-uploads'
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### File Retrieval and Management
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
// Get file metadata
|
|
393
|
+
const getFileInfo = async (fileId: string) => {
|
|
394
|
+
const file = await storageResource.get(fileId)
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
id: file.id,
|
|
398
|
+
url: file.url,
|
|
399
|
+
size: file.size,
|
|
400
|
+
type: file.type,
|
|
401
|
+
prefix: file.prefix
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// List files by prefix
|
|
406
|
+
const listFilesByPrefix = async (prefix: string) => {
|
|
407
|
+
const files = await storageResource.list({
|
|
408
|
+
filter: { prefix: prefix }
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
return files.map(file => ({
|
|
412
|
+
id: file.id,
|
|
413
|
+
url: file.url,
|
|
414
|
+
size: file.size
|
|
415
|
+
}))
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Delete file
|
|
419
|
+
const deleteFile = async (fileId: string) => {
|
|
420
|
+
await storageResource.remove(fileId)
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Configuration
|
|
425
|
+
|
|
426
|
+
### Storage Bucket Configuration
|
|
427
|
+
|
|
428
|
+
Configure storage buckets in your server configuration:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
interface ServerConfig {
|
|
432
|
+
// ... other configuration
|
|
433
|
+
storageBuckets: {
|
|
434
|
+
[bucketAlias: string]: {
|
|
435
|
+
url: string // S3 endpoint URL
|
|
436
|
+
apiKey: string // Access key ID and secret key separated by ':'
|
|
437
|
+
basePrefix: string // Base prefix for all files
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Environment Variables
|
|
444
|
+
|
|
445
|
+
Typical environment-based configuration:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const config = {
|
|
449
|
+
storageBuckets: {
|
|
450
|
+
main: {
|
|
451
|
+
url: process.env.S3_BUCKET_URL,
|
|
452
|
+
apiKey: `${process.env.S3_ACCESS_KEY}:${process.env.S3_SECRET_KEY}`,
|
|
453
|
+
basePrefix: process.env.S3_BASE_PREFIX || 'files'
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Security Considerations
|
|
460
|
+
|
|
461
|
+
### File Type Validation
|
|
462
|
+
- Always validate file types to prevent malicious uploads
|
|
463
|
+
- Use MIME type detection from file content, not just extensions
|
|
464
|
+
- Implement file size limits to prevent abuse
|
|
465
|
+
|
|
466
|
+
### Access Control
|
|
467
|
+
- Use separate buckets for different security levels
|
|
468
|
+
- Implement proper API key management and rotation
|
|
469
|
+
- Consider signed URLs for temporary access
|
|
470
|
+
|
|
471
|
+
### Storage Security
|
|
472
|
+
- Use HTTPS for all storage operations
|
|
473
|
+
- Implement proper bucket policies and access controls
|
|
474
|
+
- Monitor storage access and usage patterns
|
|
475
|
+
|
|
476
|
+
## Performance Considerations
|
|
477
|
+
|
|
478
|
+
- **Stream Processing**: Use streams for large files to minimize memory usage
|
|
479
|
+
- **File Size Limits**: Implement appropriate file size limits
|
|
480
|
+
- **Concurrent Uploads**: Consider rate limiting for multiple concurrent uploads
|
|
481
|
+
- **CDN Integration**: Use CDN for frequently accessed files
|
|
482
|
+
|
|
483
|
+
## Integration with OwlMeans Ecosystem
|
|
484
|
+
|
|
485
|
+
This package integrates with:
|
|
486
|
+
|
|
487
|
+
- **@owlmeans/resource**: Base resource management patterns
|
|
488
|
+
- **@owlmeans/server-context**: Server context and configuration
|
|
489
|
+
- **@owlmeans/storage-common**: Shared storage utilities and errors
|
|
490
|
+
|
|
491
|
+
## Best Practices
|
|
492
|
+
|
|
493
|
+
1. **Validate file types** always from content, not just filename
|
|
494
|
+
2. **Use appropriate prefixes** to organize files logically
|
|
495
|
+
3. **Implement file size limits** to prevent abuse
|
|
496
|
+
4. **Handle errors gracefully** with specific error types
|
|
497
|
+
5. **Use separate buckets** for different security levels
|
|
498
|
+
6. **Monitor storage usage** and implement cleanup policies
|
|
499
|
+
|
|
500
|
+
## Related Packages
|
|
501
|
+
|
|
502
|
+
- **@owlmeans/storage-common**: Storage utilities and error types
|
|
503
|
+
- **@owlmeans/resource**: Base resource management
|
|
504
|
+
- **@owlmeans/server-context**: Server context management
|
|
505
|
+
|
|
506
|
+
## AWS S3 Compatibility
|
|
507
|
+
|
|
508
|
+
This package works with:
|
|
509
|
+
- **AWS S3**: Native Amazon S3 service
|
|
510
|
+
- **MinIO**: Self-hosted S3-compatible storage
|
|
511
|
+
- **DigitalOcean Spaces**: S3-compatible cloud storage
|
|
512
|
+
- **Other S3-compatible services**: Any service implementing S3 API
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,eAAe,CAAA;AAEzC,eAAO,MAAM,kBAAkB,gCAAiB,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,IAAI,SAAS,EAAE,MAAM,WAAW,CAAA;AAE3D,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAA;AAEzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAG,SAAS,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAE9E,eAAO,MAAM,SAAS,GACpB,KAAK,SAAS,kBAAkB,EAChC,MAAM,SAAS,UAAU,QACnB,KAAK,KAAG,MAgBf,CAAA"}
|
package/build/helper.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const stripData = (file) => {
|
|
2
|
+
const intermediate = { ...file };
|
|
3
|
+
if (intermediate.format) {
|
|
4
|
+
delete intermediate.format;
|
|
5
|
+
}
|
|
6
|
+
intermediate.instances = Object.fromEntries(Object.entries(file.instances).map(([key, instance]) => [key, {
|
|
7
|
+
size: instance.size,
|
|
8
|
+
alias: instance.alias,
|
|
9
|
+
url: instance.url,
|
|
10
|
+
}]));
|
|
11
|
+
return JSON.parse(JSON.stringify(intermediate));
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,SAAS,GAAG,CAGvB,IAAW,EAAU,EAAE;IACvB,MAAM,YAAY,GAAU,EAAE,GAAG,IAAI,EAAE,CAAA;IAEvC,IAAI,YAAY,CAAC,MAAM,EAAE,CAAC;QACxB,OAAO,YAAY,CAAC,MAAM,CAAA;IAC5B,CAAC;IAED,YAAY,CAAC,SAAS,GAAG,MAAM,CAAC,WAAW,CACzC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE;YAC5D,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,GAAG,EAAE,QAAQ,CAAC,GAAG;SAClB,CAAC,CAAC,CACJ,CAAA;IAED,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAW,CAAA;AAC3D,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
|
package/build/model.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":""}
|
package/build/model.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Config, Context, StorageResource } from './types.js';
|
|
2
|
+
export declare const createStorageResource: (alias?: string, configKey?: string) => StorageResource;
|
|
3
|
+
export declare const appendStorageResource: <C extends Config, T extends Context<C>>(ctx: T, alias?: string, configKey?: string) => T;
|
|
4
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAgB,MAAM,YAAY,CAAA;AAShF,eAAO,MAAM,qBAAqB,WAAW,MAAM,cAA8B,MAAM,oBAuEtF,CAAA;AAED,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OACrE,CAAC,UAAS,MAAM,cAA8B,MAAM,KACxD,CAMF,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { appendContextual } from '@owlmeans/context';
|
|
2
|
+
import { DEFAULT_ALIAS } from './consts.js';
|
|
3
|
+
import { FilePropertyError, FileStreamError, FileTypeError, StorageApiError } from '@owlmeans/storage-common';
|
|
4
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
5
|
+
import { ResilientError } from '@owlmeans/error';
|
|
6
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
7
|
+
export const createStorageResource = (alias = DEFAULT_ALIAS, configKey) => {
|
|
8
|
+
configKey ??= alias;
|
|
9
|
+
const resource = appendContextual(alias, {
|
|
10
|
+
create: async (record, _opts) => {
|
|
11
|
+
if (record.stream == null) {
|
|
12
|
+
throw new FileStreamError('no');
|
|
13
|
+
}
|
|
14
|
+
if (record.size == null) {
|
|
15
|
+
throw new FilePropertyError('size');
|
|
16
|
+
}
|
|
17
|
+
// @TODO Do something here - it would be fixed if we do not convert
|
|
18
|
+
// multipart into buffer by means integrated into fastify
|
|
19
|
+
const buffers = await record.stream.toArray();
|
|
20
|
+
const buffer = buffers[0];
|
|
21
|
+
const type = await fileTypeFromBuffer(buffer);
|
|
22
|
+
if (type?.mime !== record.type) {
|
|
23
|
+
throw new FileTypeError('mime-mismatch');
|
|
24
|
+
}
|
|
25
|
+
const context = resource.assertCtx();
|
|
26
|
+
const config = context.cfg.storageBuckets[configKey];
|
|
27
|
+
const [keyId, keySecret] = config.apiKey.split(':');
|
|
28
|
+
const [bucket, ...parts] = config.url.split('.');
|
|
29
|
+
// @TODO Abstract it some way and make the upload multipart
|
|
30
|
+
try {
|
|
31
|
+
const client = new S3Client({
|
|
32
|
+
region: 'eu-central-1',
|
|
33
|
+
endpoint: `https://${parts.join('.')}`,
|
|
34
|
+
credentials: {
|
|
35
|
+
accessKeyId: keyId,
|
|
36
|
+
secretAccessKey: keySecret,
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const result = await client.send(new PutObjectCommand({
|
|
40
|
+
ACL: 'public-read',
|
|
41
|
+
Bucket: bucket,
|
|
42
|
+
Body: Buffer.concat(buffers),
|
|
43
|
+
Key: `${config.basePrefix}/${record.prefix}`,
|
|
44
|
+
ContentLength: record.size,
|
|
45
|
+
ContentDisposition: 'inline',
|
|
46
|
+
ContentType: record.type,
|
|
47
|
+
}));
|
|
48
|
+
if (result.$metadata.httpStatusCode !== 200) {
|
|
49
|
+
throw new StorageApiError('code');
|
|
50
|
+
}
|
|
51
|
+
record.url = `https://${config.url}/${config.basePrefix}/${record.prefix}`;
|
|
52
|
+
delete record.stream;
|
|
53
|
+
client.destroy();
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (e instanceof ResilientError) {
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
console.error(e);
|
|
60
|
+
throw new StorageApiError();
|
|
61
|
+
}
|
|
62
|
+
return record;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return resource;
|
|
66
|
+
};
|
|
67
|
+
export const appendStorageResource = (ctx, alias = DEFAULT_ALIAS, configKey) => {
|
|
68
|
+
const resource = createStorageResource(alias, configKey);
|
|
69
|
+
ctx.registerResource(resource);
|
|
70
|
+
return ctx;
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Resource, ResourceRecord } from '@owlmeans/resource';
|
|
2
|
+
import type { StoredFileFormat } from '@owlmeans/storage-common';
|
|
3
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context';
|
|
4
|
+
import type { Readable } from 'stream';
|
|
5
|
+
export interface StoredRecord extends ResourceRecord {
|
|
6
|
+
url?: string;
|
|
7
|
+
size?: number;
|
|
8
|
+
prefix: string;
|
|
9
|
+
stream?: Readable;
|
|
10
|
+
format?: StoredFileFormat;
|
|
11
|
+
type?: string;
|
|
12
|
+
bytes?: Uint8Array;
|
|
13
|
+
base64?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface StoredConfigAppend {
|
|
16
|
+
storageBuckets: {
|
|
17
|
+
[key: string]: StorageConfig;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface StorageConfig {
|
|
21
|
+
url: string;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
basePrefix: string;
|
|
24
|
+
}
|
|
25
|
+
export interface StorageResource extends Resource<StoredRecord> {
|
|
26
|
+
}
|
|
27
|
+
export interface Config extends ServerConfig, StoredConfigAppend {
|
|
28
|
+
}
|
|
29
|
+
export interface Context<C extends Config = Config> extends ServerContext<C> {
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAEtC,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,QAAQ,CAAA;IACjB,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAA;CACjD;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAgB,SAAQ,QAAQ,CAAC,YAAY,CAAC;CAC9D;AAED,MAAM,WAAW,MAAO,SAAQ,YAAY,EAAE,kBAAkB;CAAI;AACpE,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/storage-resource",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 393 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": "*"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@aws-sdk/client-s3": "^3.701.0",
|
|
27
|
+
"@owlmeans/context": "^0.1.0",
|
|
28
|
+
"@owlmeans/error": "^0.1.0",
|
|
29
|
+
"@owlmeans/resource": "^0.1.0",
|
|
30
|
+
"@owlmeans/server-context": "^0.1.0",
|
|
31
|
+
"@owlmeans/storage-common": "^0.1.0",
|
|
32
|
+
"file-type": "^19.6.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.7.8",
|
|
36
|
+
"nodemon": "^3.1.7",
|
|
37
|
+
"typescript": "^5.6.3"
|
|
38
|
+
},
|
|
39
|
+
"private": false,
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/consts.ts
ADDED
package/src/helper.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StoredFile, StoredFileWithData } from '@owlmeans/storage-common'
|
|
2
|
+
|
|
3
|
+
export const stripData = <
|
|
4
|
+
Input extends StoredFileWithData,
|
|
5
|
+
Output extends StoredFile
|
|
6
|
+
>(file: Input): Output => {
|
|
7
|
+
const intermediate: Input = { ...file }
|
|
8
|
+
|
|
9
|
+
if (intermediate.format) {
|
|
10
|
+
delete intermediate.format
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
intermediate.instances = Object.fromEntries(
|
|
14
|
+
Object.entries(file.instances).map(([key, instance]) => [key, {
|
|
15
|
+
size: instance.size,
|
|
16
|
+
alias: instance.alias,
|
|
17
|
+
url: instance.url,
|
|
18
|
+
}])
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return JSON.parse(JSON.stringify(intermediate)) as Output
|
|
22
|
+
}
|
package/src/index.ts
ADDED
package/src/model.ts
ADDED
|
File without changes
|
package/src/resource.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { appendContextual } from '@owlmeans/context'
|
|
2
|
+
import { DEFAULT_ALIAS } from './consts.js'
|
|
3
|
+
import type { Config, Context, StorageResource, StoredRecord } from './types.js'
|
|
4
|
+
import type { GetterOptions } from '@owlmeans/resource'
|
|
5
|
+
import { FilePropertyError, FileStreamError, FileTypeError, StorageApiError } from '@owlmeans/storage-common'
|
|
6
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
|
|
7
|
+
import { ResilientError } from '@owlmeans/error'
|
|
8
|
+
import { fileTypeFromBuffer } from 'file-type'
|
|
9
|
+
|
|
10
|
+
type Getter = string | GetterOptions
|
|
11
|
+
|
|
12
|
+
export const createStorageResource = (alias: string = DEFAULT_ALIAS, configKey?: string) => {
|
|
13
|
+
configKey ??= alias
|
|
14
|
+
|
|
15
|
+
const resource = appendContextual<StorageResource>(alias, {
|
|
16
|
+
create: async <Type extends StoredRecord>(record: Partial<Type>, _opts?: Getter) => {
|
|
17
|
+
if (record.stream == null) {
|
|
18
|
+
throw new FileStreamError('no')
|
|
19
|
+
}
|
|
20
|
+
if (record.size == null) {
|
|
21
|
+
throw new FilePropertyError('size')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// @TODO Do something here - it would be fixed if we do not convert
|
|
26
|
+
// multipart into buffer by means integrated into fastify
|
|
27
|
+
const buffers = await record.stream.toArray()
|
|
28
|
+
const buffer = buffers[0]
|
|
29
|
+
const type = await fileTypeFromBuffer(buffer)
|
|
30
|
+
|
|
31
|
+
if (type?.mime !== record.type) {
|
|
32
|
+
throw new FileTypeError('mime-mismatch')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const context = resource.assertCtx() as Context
|
|
36
|
+
|
|
37
|
+
const config = context.cfg.storageBuckets[configKey]
|
|
38
|
+
const [keyId, keySecret] = config.apiKey.split(':')
|
|
39
|
+
const [bucket, ...parts] = config.url.split('.')
|
|
40
|
+
|
|
41
|
+
// @TODO Abstract it some way and make the upload multipart
|
|
42
|
+
try {
|
|
43
|
+
const client = new S3Client({
|
|
44
|
+
region: 'eu-central-1',
|
|
45
|
+
endpoint: `https://${parts.join('.')}`,
|
|
46
|
+
credentials: {
|
|
47
|
+
accessKeyId: keyId,
|
|
48
|
+
secretAccessKey: keySecret,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const result = await client.send(new PutObjectCommand({
|
|
53
|
+
ACL: 'public-read',
|
|
54
|
+
Bucket: bucket,
|
|
55
|
+
Body: Buffer.concat(buffers),
|
|
56
|
+
Key: `${config.basePrefix}/${record.prefix}`,
|
|
57
|
+
ContentLength: record.size,
|
|
58
|
+
ContentDisposition: 'inline',
|
|
59
|
+
ContentType: record.type,
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
if (result.$metadata.httpStatusCode !== 200) {
|
|
63
|
+
throw new StorageApiError('code')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
record.url = `https://${config.url}/${config.basePrefix}/${record.prefix}`
|
|
67
|
+
delete record.stream
|
|
68
|
+
|
|
69
|
+
client.destroy()
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (e instanceof ResilientError) {
|
|
72
|
+
throw e
|
|
73
|
+
}
|
|
74
|
+
console.error(e)
|
|
75
|
+
throw new StorageApiError()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return record as Type
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return resource
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const appendStorageResource = <C extends Config, T extends Context<C>>(
|
|
86
|
+
ctx: T, alias: string = DEFAULT_ALIAS, configKey?: string
|
|
87
|
+
): T => {
|
|
88
|
+
const resource = createStorageResource(alias, configKey)
|
|
89
|
+
|
|
90
|
+
ctx.registerResource(resource)
|
|
91
|
+
|
|
92
|
+
return ctx
|
|
93
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Resource, ResourceRecord } from '@owlmeans/resource'
|
|
2
|
+
import type { StoredFileFormat } from '@owlmeans/storage-common'
|
|
3
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
4
|
+
import type { Readable } from 'stream'
|
|
5
|
+
|
|
6
|
+
export interface StoredRecord extends ResourceRecord {
|
|
7
|
+
url?: string
|
|
8
|
+
size?: number
|
|
9
|
+
prefix: string
|
|
10
|
+
stream?: Readable
|
|
11
|
+
format?: StoredFileFormat
|
|
12
|
+
type?: string,
|
|
13
|
+
bytes?: Uint8Array
|
|
14
|
+
base64?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StoredConfigAppend {
|
|
18
|
+
storageBuckets: { [key: string]: StorageConfig }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StorageConfig {
|
|
22
|
+
url: string
|
|
23
|
+
apiKey: string
|
|
24
|
+
basePrefix: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface StorageResource extends Resource<StoredRecord> {
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Config extends ServerConfig, StoredConfigAppend { }
|
|
31
|
+
export interface Context<C extends Config = Config> extends ServerContext<C> { }
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"../tsconfig.default.json",
|
|
4
|
+
],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
7
|
+
"outDir": "./build/", /* Specify an output folder for all emitted files. */
|
|
8
|
+
"moduleResolution": "Bundler",
|
|
9
|
+
},
|
|
10
|
+
"exclude": [
|
|
11
|
+
"./dist/**/*",
|
|
12
|
+
"./build/**/*",
|
|
13
|
+
"./*.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/consts.ts","./src/helper.ts","./src/index.ts","./src/model.ts","./src/resource.ts","./src/types.ts"],"version":"5.6.3"}
|