@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 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,3 @@
1
+ export declare const DEFAULT_ALIAS = "s3-storage";
2
+ export declare const supportedMimeTypes: import("file-type").MimeType[];
3
+ //# sourceMappingURL=consts.d.ts.map
@@ -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"}
@@ -0,0 +1,4 @@
1
+ import { supportedMimeTypes as mimeTypes } from 'file-type';
2
+ export const DEFAULT_ALIAS = 's3-storage';
3
+ export const supportedMimeTypes = [...mimeTypes];
4
+ //# sourceMappingURL=consts.js.map
@@ -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,3 @@
1
+ import type { StoredFile, StoredFileWithData } from '@owlmeans/storage-common';
2
+ export declare const stripData: <Input extends StoredFileWithData, Output extends StoredFile>(file: Input) => Output;
3
+ //# sourceMappingURL=helper.d.ts.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,5 @@
1
+ export type * from './types.js';
2
+ export * from './consts.js';
3
+ export * from './helper.js';
4
+ export * from './resource.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,4 @@
1
+ export * from './consts.js';
2
+ export * from './helper.js';
3
+ export * from './resource.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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,2 @@
1
+ "use strict";
2
+ //# sourceMappingURL=model.js.map
@@ -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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -0,0 +1,5 @@
1
+ import { supportedMimeTypes as mimeTypes } from 'file-type'
2
+
3
+ export const DEFAULT_ALIAS = 's3-storage'
4
+
5
+ export const supportedMimeTypes = [...mimeTypes]
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
@@ -0,0 +1,5 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './consts.js'
4
+ export * from './helper.js'
5
+ export * from './resource.js'
package/src/model.ts ADDED
File without changes
@@ -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"}