@opensaas/stack-storage 0.1.1
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +426 -0
- package/LICENSE +21 -0
- package/README.md +425 -0
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +25 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +113 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/fields/index.d.ts +111 -0
- package/dist/fields/index.d.ts.map +1 -0
- package/dist/fields/index.js +237 -0
- package/dist/fields/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/local.d.ts +22 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +64 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/runtime/index.d.ts +75 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +157 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/utils/image.d.ts +18 -0
- package/dist/utils/image.d.ts.map +1 -0
- package/dist/utils/image.js +82 -0
- package/dist/utils/image.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/upload.d.ts +56 -0
- package/dist/utils/upload.d.ts.map +1 -0
- package/dist/utils/upload.js +74 -0
- package/dist/utils/upload.js.map +1 -0
- package/package.json +50 -0
- package/src/config/index.ts +30 -0
- package/src/config/types.ts +127 -0
- package/src/fields/index.ts +347 -0
- package/src/index.ts +14 -0
- package/src/providers/index.ts +1 -0
- package/src/providers/local.ts +85 -0
- package/src/runtime/index.ts +243 -0
- package/src/utils/image.ts +111 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/upload.ts +122 -0
- package/tests/image-utils.test.ts +498 -0
- package/tests/local-provider.test.ts +349 -0
- package/tests/upload-utils.test.ts +313 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { LocalStorageProvider } from '../providers/local.js';
|
|
2
|
+
import { validateFile, getMimeType } from '../utils/upload.js';
|
|
3
|
+
import { getImageDimensions, processImageTransformations } from '../utils/image.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a storage provider instance from config
|
|
6
|
+
*/
|
|
7
|
+
export function createStorageProvider(config, providerName) {
|
|
8
|
+
if (!config.storage || !config.storage[providerName]) {
|
|
9
|
+
throw new Error(`Storage provider '${providerName}' not found in config`);
|
|
10
|
+
}
|
|
11
|
+
const providerConfig = config.storage[providerName];
|
|
12
|
+
switch (providerConfig.type) {
|
|
13
|
+
case 'local':
|
|
14
|
+
return new LocalStorageProvider(providerConfig);
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(`Unknown storage provider type: ${providerConfig.type}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Uploads a file to the specified storage provider
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const metadata = await uploadFile(config, 'documents', {
|
|
25
|
+
* file,
|
|
26
|
+
* buffer,
|
|
27
|
+
* validation: {
|
|
28
|
+
* maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
29
|
+
* acceptedMimeTypes: ['application/pdf']
|
|
30
|
+
* }
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export async function uploadFile(config, storageProviderName, data, options) {
|
|
35
|
+
const { file, buffer } = data;
|
|
36
|
+
// Validate file
|
|
37
|
+
if (options?.validation) {
|
|
38
|
+
const validation = validateFile({
|
|
39
|
+
size: file.size,
|
|
40
|
+
name: file.name,
|
|
41
|
+
type: file.type,
|
|
42
|
+
}, options.validation);
|
|
43
|
+
if (!validation.valid) {
|
|
44
|
+
throw new Error(validation.error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Get storage provider
|
|
48
|
+
const provider = createStorageProvider(config, storageProviderName);
|
|
49
|
+
// Determine content type
|
|
50
|
+
const contentType = file.type || getMimeType(file.name);
|
|
51
|
+
// Upload file
|
|
52
|
+
const result = await provider.upload(buffer, file.name, {
|
|
53
|
+
contentType,
|
|
54
|
+
metadata: options?.metadata,
|
|
55
|
+
});
|
|
56
|
+
// Return metadata
|
|
57
|
+
return {
|
|
58
|
+
filename: result.filename,
|
|
59
|
+
originalFilename: file.name,
|
|
60
|
+
url: result.url,
|
|
61
|
+
mimeType: contentType,
|
|
62
|
+
size: result.size,
|
|
63
|
+
uploadedAt: new Date().toISOString(),
|
|
64
|
+
storageProvider: storageProviderName,
|
|
65
|
+
metadata: result.metadata,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Uploads an image with optional transformations
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const metadata = await uploadImage(config, 'avatars', {
|
|
74
|
+
* file,
|
|
75
|
+
* buffer,
|
|
76
|
+
* validation: {
|
|
77
|
+
* maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
78
|
+
* acceptedMimeTypes: ['image/jpeg', 'image/png', 'image/webp']
|
|
79
|
+
* },
|
|
80
|
+
* transformations: {
|
|
81
|
+
* thumbnail: { width: 100, height: 100, fit: 'cover' },
|
|
82
|
+
* profile: { width: 400, height: 400, fit: 'cover' }
|
|
83
|
+
* }
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export async function uploadImage(config, storageProviderName, data, options) {
|
|
88
|
+
const { file, buffer } = data;
|
|
89
|
+
// Validate file
|
|
90
|
+
if (options?.validation) {
|
|
91
|
+
const validation = validateFile({
|
|
92
|
+
size: file.size,
|
|
93
|
+
name: file.name,
|
|
94
|
+
type: file.type,
|
|
95
|
+
}, options.validation);
|
|
96
|
+
if (!validation.valid) {
|
|
97
|
+
throw new Error(validation.error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Get storage provider
|
|
101
|
+
const provider = createStorageProvider(config, storageProviderName);
|
|
102
|
+
// Determine content type
|
|
103
|
+
const contentType = file.type || getMimeType(file.name);
|
|
104
|
+
// Get original image dimensions
|
|
105
|
+
const { width, height } = await getImageDimensions(buffer);
|
|
106
|
+
// Upload original image
|
|
107
|
+
const result = await provider.upload(buffer, file.name, {
|
|
108
|
+
contentType,
|
|
109
|
+
metadata: options?.metadata,
|
|
110
|
+
});
|
|
111
|
+
// Process transformations if provided
|
|
112
|
+
let transformations;
|
|
113
|
+
if (options?.transformations) {
|
|
114
|
+
transformations = await processImageTransformations(buffer, file.name, options.transformations, provider, contentType);
|
|
115
|
+
}
|
|
116
|
+
// Return metadata
|
|
117
|
+
return {
|
|
118
|
+
filename: result.filename,
|
|
119
|
+
originalFilename: file.name,
|
|
120
|
+
url: result.url,
|
|
121
|
+
mimeType: contentType,
|
|
122
|
+
size: result.size,
|
|
123
|
+
width,
|
|
124
|
+
height,
|
|
125
|
+
uploadedAt: new Date().toISOString(),
|
|
126
|
+
storageProvider: storageProviderName,
|
|
127
|
+
metadata: result.metadata,
|
|
128
|
+
transformations,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Deletes a file from storage
|
|
133
|
+
*/
|
|
134
|
+
export async function deleteFile(config, storageProviderName, filename) {
|
|
135
|
+
const provider = createStorageProvider(config, storageProviderName);
|
|
136
|
+
await provider.delete(filename);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Deletes an image and all its transformations from storage
|
|
140
|
+
*/
|
|
141
|
+
export async function deleteImage(config, metadata) {
|
|
142
|
+
const provider = createStorageProvider(config, metadata.storageProvider);
|
|
143
|
+
// Delete original image
|
|
144
|
+
await provider.delete(metadata.filename);
|
|
145
|
+
// Delete all transformations
|
|
146
|
+
if (metadata.transformations) {
|
|
147
|
+
for (const transformationResult of Object.values(metadata.transformations)) {
|
|
148
|
+
// Extract filename from URL
|
|
149
|
+
const filename = transformationResult.url.split('/').pop();
|
|
150
|
+
if (filename) {
|
|
151
|
+
await provider.delete(filename);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export { parseFileFromFormData } from '../utils/upload.js';
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ImageTransformationConfig, ImageTransformationResult, StorageProvider } from '../config/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Gets image dimensions from a buffer
|
|
4
|
+
*/
|
|
5
|
+
export declare function getImageDimensions(buffer: Buffer | Uint8Array): Promise<{
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Applies a single transformation to an image
|
|
11
|
+
*/
|
|
12
|
+
export declare function transformImage(buffer: Buffer | Uint8Array, transformation: ImageTransformationConfig): Promise<Buffer>;
|
|
13
|
+
/**
|
|
14
|
+
* Processes all transformations for an image
|
|
15
|
+
* Uploads the original and all transformed versions
|
|
16
|
+
*/
|
|
17
|
+
export declare function processImageTransformations(buffer: Buffer | Uint8Array, originalFilename: string, transformations: Record<string, ImageTransformationConfig>, storageProvider: StorageProvider, contentType: string): Promise<Record<string, ImageTransformationResult>>;
|
|
18
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/utils/image.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,eAAe,EAChB,MAAM,oBAAoB,CAAA;AAE3B;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAM5C;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,cAAc,EAAE,yBAAyB,GACxC,OAAO,CAAC,MAAM,CAAC,CAmCjB;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,gBAAgB,EAAE,MAAM,EACxB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,EAC1D,eAAe,EAAE,eAAe,EAChC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAqCpD"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
/**
|
|
3
|
+
* Gets image dimensions from a buffer
|
|
4
|
+
*/
|
|
5
|
+
export async function getImageDimensions(buffer) {
|
|
6
|
+
const metadata = await sharp(buffer).metadata();
|
|
7
|
+
return {
|
|
8
|
+
width: metadata.width || 0,
|
|
9
|
+
height: metadata.height || 0,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Applies a single transformation to an image
|
|
14
|
+
*/
|
|
15
|
+
export async function transformImage(buffer, transformation) {
|
|
16
|
+
let image = sharp(buffer);
|
|
17
|
+
// Apply resizing
|
|
18
|
+
if (transformation.width || transformation.height) {
|
|
19
|
+
image = image.resize({
|
|
20
|
+
width: transformation.width,
|
|
21
|
+
height: transformation.height,
|
|
22
|
+
fit: transformation.fit || 'cover',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Apply format conversion
|
|
26
|
+
if (transformation.format) {
|
|
27
|
+
const options = {
|
|
28
|
+
quality: transformation.quality || 80,
|
|
29
|
+
};
|
|
30
|
+
switch (transformation.format) {
|
|
31
|
+
case 'jpeg':
|
|
32
|
+
image = image.jpeg(options);
|
|
33
|
+
break;
|
|
34
|
+
case 'png':
|
|
35
|
+
image = image.png(options);
|
|
36
|
+
break;
|
|
37
|
+
case 'webp':
|
|
38
|
+
image = image.webp(options);
|
|
39
|
+
break;
|
|
40
|
+
case 'avif':
|
|
41
|
+
image = image.avif(options);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return await image.toBuffer();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Processes all transformations for an image
|
|
49
|
+
* Uploads the original and all transformed versions
|
|
50
|
+
*/
|
|
51
|
+
export async function processImageTransformations(buffer, originalFilename, transformations, storageProvider, contentType) {
|
|
52
|
+
const results = {};
|
|
53
|
+
for (const [name, config] of Object.entries(transformations)) {
|
|
54
|
+
// Transform the image
|
|
55
|
+
const transformedBuffer = await transformImage(buffer, config);
|
|
56
|
+
// Get dimensions of transformed image
|
|
57
|
+
const { width, height } = await getImageDimensions(transformedBuffer);
|
|
58
|
+
// Generate filename for transformation
|
|
59
|
+
const ext = config.format ? `.${config.format}` : '';
|
|
60
|
+
const transformedFilename = `${originalFilename}-${name}${ext}`;
|
|
61
|
+
// Upload transformed image
|
|
62
|
+
const uploadResult = await storageProvider.upload(transformedBuffer, transformedFilename, {
|
|
63
|
+
contentType: config.format === 'jpeg'
|
|
64
|
+
? 'image/jpeg'
|
|
65
|
+
: config.format === 'png'
|
|
66
|
+
? 'image/png'
|
|
67
|
+
: config.format === 'webp'
|
|
68
|
+
? 'image/webp'
|
|
69
|
+
: config.format === 'avif'
|
|
70
|
+
? 'image/avif'
|
|
71
|
+
: contentType,
|
|
72
|
+
});
|
|
73
|
+
results[name] = {
|
|
74
|
+
url: uploadResult.url,
|
|
75
|
+
width,
|
|
76
|
+
height,
|
|
77
|
+
size: uploadResult.size,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/utils/image.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File validation options
|
|
3
|
+
*/
|
|
4
|
+
export interface FileValidationOptions {
|
|
5
|
+
/** Maximum file size in bytes */
|
|
6
|
+
maxFileSize?: number;
|
|
7
|
+
/** Accepted MIME types (e.g., ['image/jpeg', 'image/png']) */
|
|
8
|
+
acceptedMimeTypes?: string[];
|
|
9
|
+
/** Accepted file extensions (e.g., ['.jpg', '.png']) */
|
|
10
|
+
acceptedExtensions?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* File validation result
|
|
14
|
+
*/
|
|
15
|
+
export interface FileValidationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validates a file against the provided options
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateFile(file: {
|
|
23
|
+
size: number;
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
}, options?: FileValidationOptions): FileValidationResult;
|
|
27
|
+
/**
|
|
28
|
+
* Formats file size in human-readable format
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatFileSize(bytes: number): string;
|
|
31
|
+
/**
|
|
32
|
+
* Gets MIME type from filename
|
|
33
|
+
*/
|
|
34
|
+
export declare function getMimeType(filename: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Extracts file metadata from a File or Blob
|
|
37
|
+
*/
|
|
38
|
+
export interface FileInfo {
|
|
39
|
+
name: string;
|
|
40
|
+
size: number;
|
|
41
|
+
type: string;
|
|
42
|
+
lastModified?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Converts a File/Blob to Buffer for Node.js processing
|
|
46
|
+
*/
|
|
47
|
+
export declare function fileToBuffer(file: Blob | File): Promise<Buffer>;
|
|
48
|
+
/**
|
|
49
|
+
* Parses FormData and extracts file information
|
|
50
|
+
* This is a utility for developers to use in their upload routes
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseFileFromFormData(formData: FormData, fieldName?: string): Promise<{
|
|
53
|
+
file: File;
|
|
54
|
+
buffer: Buffer;
|
|
55
|
+
} | null>;
|
|
56
|
+
//# sourceMappingURL=upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/utils/upload.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAClD,OAAO,CAAC,EAAE,qBAAqB,GAC9B,oBAAoB,CAoCtB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrE;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,QAAQ,EAClB,SAAS,GAAE,MAAe,GACzB,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAUhD"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import mime from 'mime-types';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a file against the provided options
|
|
4
|
+
*/
|
|
5
|
+
export function validateFile(file, options) {
|
|
6
|
+
if (!options) {
|
|
7
|
+
return { valid: true };
|
|
8
|
+
}
|
|
9
|
+
// Check file size
|
|
10
|
+
if (options.maxFileSize && file.size > options.maxFileSize) {
|
|
11
|
+
return {
|
|
12
|
+
valid: false,
|
|
13
|
+
error: `File size exceeds maximum allowed size of ${formatFileSize(options.maxFileSize)}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// Check MIME type
|
|
17
|
+
if (options.acceptedMimeTypes && options.acceptedMimeTypes.length > 0) {
|
|
18
|
+
const fileMimeType = file.type || mime.lookup(file.name) || '';
|
|
19
|
+
if (!options.acceptedMimeTypes.includes(fileMimeType)) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
error: `File type '${fileMimeType}' is not allowed. Accepted types: ${options.acceptedMimeTypes.join(', ')}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Check file extension
|
|
27
|
+
if (options.acceptedExtensions && options.acceptedExtensions.length > 0) {
|
|
28
|
+
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
29
|
+
if (!options.acceptedExtensions.includes(ext)) {
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
error: `File extension '${ext}' is not allowed. Accepted extensions: ${options.acceptedExtensions.join(', ')}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { valid: true };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Formats file size in human-readable format
|
|
40
|
+
*/
|
|
41
|
+
export function formatFileSize(bytes) {
|
|
42
|
+
if (bytes === 0)
|
|
43
|
+
return '0 Bytes';
|
|
44
|
+
const k = 1024;
|
|
45
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
46
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
47
|
+
return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Gets MIME type from filename
|
|
51
|
+
*/
|
|
52
|
+
export function getMimeType(filename) {
|
|
53
|
+
return mime.lookup(filename) || 'application/octet-stream';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Converts a File/Blob to Buffer for Node.js processing
|
|
57
|
+
*/
|
|
58
|
+
export async function fileToBuffer(file) {
|
|
59
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
60
|
+
return Buffer.from(arrayBuffer);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parses FormData and extracts file information
|
|
64
|
+
* This is a utility for developers to use in their upload routes
|
|
65
|
+
*/
|
|
66
|
+
export async function parseFileFromFormData(formData, fieldName = 'file') {
|
|
67
|
+
const file = formData.get(fieldName);
|
|
68
|
+
if (!file || !(file instanceof File)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const buffer = await fileToBuffer(file);
|
|
72
|
+
return { file, buffer };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/utils/upload.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opensaas/stack-storage",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "File and image upload field types with pluggable storage providers for OpenSaas Stack",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./fields": {
|
|
12
|
+
"types": "./dist/fields/index.d.ts",
|
|
13
|
+
"default": "./dist/fields/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./providers": {
|
|
16
|
+
"types": "./dist/providers/index.d.ts",
|
|
17
|
+
"default": "./dist/providers/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./runtime": {
|
|
20
|
+
"types": "./dist/runtime/index.d.ts",
|
|
21
|
+
"default": "./dist/runtime/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"mime-types": "^3.0.1",
|
|
28
|
+
"sharp": "^0.34.4",
|
|
29
|
+
"zod": "^4.1.12",
|
|
30
|
+
"@opensaas/stack-core": "0.1.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/mime-types": "^3.0.1",
|
|
34
|
+
"@types/node": "^24.0.0",
|
|
35
|
+
"@types/react": "^19.2.2",
|
|
36
|
+
"@vitest/coverage-v8": "^4.0.5",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"vitest": "^4.0.5"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@opensaas/stack-core": "0.1.1"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"dev": "tsc --watch",
|
|
46
|
+
"test": "vitest",
|
|
47
|
+
"test:ui": "vitest --ui",
|
|
48
|
+
"test:coverage": "vitest --coverage"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LocalStorageConfig } from './types.js'
|
|
2
|
+
|
|
3
|
+
export * from './types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a local filesystem storage configuration
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const config = config({
|
|
11
|
+
* storage: {
|
|
12
|
+
* documents: localStorage({
|
|
13
|
+
* uploadDir: './uploads/documents',
|
|
14
|
+
* serveUrl: '/api/files',
|
|
15
|
+
* }),
|
|
16
|
+
* },
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function localStorage(
|
|
21
|
+
config: Pick<LocalStorageConfig, 'uploadDir' | 'serveUrl'> &
|
|
22
|
+
Partial<Pick<LocalStorageConfig, 'generateUniqueFilenames'>>,
|
|
23
|
+
): LocalStorageConfig {
|
|
24
|
+
return {
|
|
25
|
+
type: 'local',
|
|
26
|
+
uploadDir: config.uploadDir,
|
|
27
|
+
serveUrl: config.serveUrl,
|
|
28
|
+
generateUniqueFilenames: config.generateUniqueFilenames ?? true,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage provider interface that all storage backends must implement.
|
|
3
|
+
* This allows pluggable storage solutions (local, S3, Vercel Blob, etc.)
|
|
4
|
+
*/
|
|
5
|
+
export interface StorageProvider {
|
|
6
|
+
/**
|
|
7
|
+
* Uploads a file to the storage provider
|
|
8
|
+
* @param file - File data as Buffer or Uint8Array
|
|
9
|
+
* @param filename - Desired filename (may be transformed by provider)
|
|
10
|
+
* @param options - Additional upload options (contentType, metadata, etc.)
|
|
11
|
+
* @returns Upload result with URL and metadata
|
|
12
|
+
*/
|
|
13
|
+
upload(
|
|
14
|
+
file: Buffer | Uint8Array,
|
|
15
|
+
filename: string,
|
|
16
|
+
options?: UploadOptions,
|
|
17
|
+
): Promise<UploadResult>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Downloads a file from the storage provider
|
|
21
|
+
* @param filename - Filename to download
|
|
22
|
+
* @returns File data as Buffer
|
|
23
|
+
*/
|
|
24
|
+
download(filename: string): Promise<Buffer>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Deletes a file from the storage provider
|
|
28
|
+
* @param filename - Filename to delete
|
|
29
|
+
*/
|
|
30
|
+
delete(filename: string): Promise<void>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the public URL for a file
|
|
34
|
+
* @param filename - Filename to get URL for
|
|
35
|
+
* @returns Public URL string
|
|
36
|
+
*/
|
|
37
|
+
getUrl(filename: string): string
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Optional: Gets a signed URL for private files
|
|
41
|
+
* @param filename - Filename to get signed URL for
|
|
42
|
+
* @param expiresIn - Expiration time in seconds
|
|
43
|
+
* @returns Signed URL string
|
|
44
|
+
*/
|
|
45
|
+
getSignedUrl?(filename: string, expiresIn?: number): Promise<string>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options for uploading a file
|
|
50
|
+
*/
|
|
51
|
+
export interface UploadOptions {
|
|
52
|
+
/** MIME type of the file */
|
|
53
|
+
contentType?: string
|
|
54
|
+
/** Custom metadata to store with the file */
|
|
55
|
+
metadata?: Record<string, string>
|
|
56
|
+
/** Whether the file should be publicly accessible */
|
|
57
|
+
public?: boolean
|
|
58
|
+
/** Cache control header */
|
|
59
|
+
cacheControl?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Result from uploading a file
|
|
64
|
+
*/
|
|
65
|
+
export interface UploadResult {
|
|
66
|
+
/** Generated filename (may differ from input) */
|
|
67
|
+
filename: string
|
|
68
|
+
/** Public URL to access the file */
|
|
69
|
+
url: string
|
|
70
|
+
/** File size in bytes */
|
|
71
|
+
size: number
|
|
72
|
+
/** MIME type */
|
|
73
|
+
contentType: string
|
|
74
|
+
/** Additional provider-specific metadata */
|
|
75
|
+
metadata?: Record<string, unknown>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Configuration for local filesystem storage
|
|
80
|
+
*/
|
|
81
|
+
export interface LocalStorageConfig {
|
|
82
|
+
type: 'local'
|
|
83
|
+
/** Directory to store uploaded files */
|
|
84
|
+
uploadDir: string
|
|
85
|
+
/** Base URL for serving files (e.g., '/api/files' or 'https://cdn.example.com') */
|
|
86
|
+
serveUrl: string
|
|
87
|
+
/** Whether to generate unique filenames (default: true) */
|
|
88
|
+
generateUniqueFilenames?: boolean
|
|
89
|
+
/** Allow additional properties */
|
|
90
|
+
[key: string]: unknown
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Base configuration shared by all storage providers
|
|
95
|
+
*/
|
|
96
|
+
export interface BaseStorageConfig {
|
|
97
|
+
type: string
|
|
98
|
+
[key: string]: unknown
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Storage configuration - maps names to storage provider configs
|
|
103
|
+
* Example: { avatars: s3Config, documents: localConfig }
|
|
104
|
+
*/
|
|
105
|
+
export type StorageConfig = Record<string, BaseStorageConfig | LocalStorageConfig>
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Re-export metadata types from core package
|
|
109
|
+
* These types are now defined in @opensaas/stack-core to avoid circular dependencies
|
|
110
|
+
*/
|
|
111
|
+
export type { FileMetadata, ImageMetadata, ImageTransformationResult } from '@opensaas/stack-core'
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Configuration for image transformations
|
|
115
|
+
*/
|
|
116
|
+
export interface ImageTransformationConfig {
|
|
117
|
+
/** Target width in pixels */
|
|
118
|
+
width?: number
|
|
119
|
+
/** Target height in pixels */
|
|
120
|
+
height?: number
|
|
121
|
+
/** Fit mode: cover (crop), contain (letterbox), fill (stretch) */
|
|
122
|
+
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'
|
|
123
|
+
/** Output format (default: original format) */
|
|
124
|
+
format?: 'jpeg' | 'png' | 'webp' | 'avif'
|
|
125
|
+
/** Quality 1-100 (default: 80) */
|
|
126
|
+
quality?: number
|
|
127
|
+
}
|