@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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +11 -0
  3. package/CLAUDE.md +426 -0
  4. package/LICENSE +21 -0
  5. package/README.md +425 -0
  6. package/dist/config/index.d.ts +19 -0
  7. package/dist/config/index.d.ts.map +1 -0
  8. package/dist/config/index.js +25 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/config/types.d.ts +113 -0
  11. package/dist/config/types.d.ts.map +1 -0
  12. package/dist/config/types.js +2 -0
  13. package/dist/config/types.js.map +1 -0
  14. package/dist/fields/index.d.ts +111 -0
  15. package/dist/fields/index.d.ts.map +1 -0
  16. package/dist/fields/index.js +237 -0
  17. package/dist/fields/index.js.map +1 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +11 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/providers/index.d.ts +2 -0
  23. package/dist/providers/index.d.ts.map +1 -0
  24. package/dist/providers/index.js +2 -0
  25. package/dist/providers/index.js.map +1 -0
  26. package/dist/providers/local.d.ts +22 -0
  27. package/dist/providers/local.d.ts.map +1 -0
  28. package/dist/providers/local.js +64 -0
  29. package/dist/providers/local.js.map +1 -0
  30. package/dist/runtime/index.d.ts +75 -0
  31. package/dist/runtime/index.d.ts.map +1 -0
  32. package/dist/runtime/index.js +157 -0
  33. package/dist/runtime/index.js.map +1 -0
  34. package/dist/utils/image.d.ts +18 -0
  35. package/dist/utils/image.d.ts.map +1 -0
  36. package/dist/utils/image.js +82 -0
  37. package/dist/utils/image.js.map +1 -0
  38. package/dist/utils/index.d.ts +3 -0
  39. package/dist/utils/index.d.ts.map +1 -0
  40. package/dist/utils/index.js +3 -0
  41. package/dist/utils/index.js.map +1 -0
  42. package/dist/utils/upload.d.ts +56 -0
  43. package/dist/utils/upload.d.ts.map +1 -0
  44. package/dist/utils/upload.js +74 -0
  45. package/dist/utils/upload.js.map +1 -0
  46. package/package.json +50 -0
  47. package/src/config/index.ts +30 -0
  48. package/src/config/types.ts +127 -0
  49. package/src/fields/index.ts +347 -0
  50. package/src/index.ts +14 -0
  51. package/src/providers/index.ts +1 -0
  52. package/src/providers/local.ts +85 -0
  53. package/src/runtime/index.ts +243 -0
  54. package/src/utils/image.ts +111 -0
  55. package/src/utils/index.ts +2 -0
  56. package/src/utils/upload.ts +122 -0
  57. package/tests/image-utils.test.ts +498 -0
  58. package/tests/local-provider.test.ts +349 -0
  59. package/tests/upload-utils.test.ts +313 -0
  60. package/tsconfig.json +9 -0
  61. package/vitest.config.ts +14 -0
@@ -0,0 +1,111 @@
1
+ import sharp from 'sharp'
2
+ import type {
3
+ ImageTransformationConfig,
4
+ ImageTransformationResult,
5
+ StorageProvider,
6
+ } from '../config/types.js'
7
+
8
+ /**
9
+ * Gets image dimensions from a buffer
10
+ */
11
+ export async function getImageDimensions(
12
+ buffer: Buffer | Uint8Array,
13
+ ): Promise<{ width: number; height: number }> {
14
+ const metadata = await sharp(buffer).metadata()
15
+ return {
16
+ width: metadata.width || 0,
17
+ height: metadata.height || 0,
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Applies a single transformation to an image
23
+ */
24
+ export async function transformImage(
25
+ buffer: Buffer | Uint8Array,
26
+ transformation: ImageTransformationConfig,
27
+ ): Promise<Buffer> {
28
+ let image = sharp(buffer)
29
+
30
+ // Apply resizing
31
+ if (transformation.width || transformation.height) {
32
+ image = image.resize({
33
+ width: transformation.width,
34
+ height: transformation.height,
35
+ fit: transformation.fit || 'cover',
36
+ })
37
+ }
38
+
39
+ // Apply format conversion
40
+ if (transformation.format) {
41
+ const options = {
42
+ quality: transformation.quality || 80,
43
+ }
44
+
45
+ switch (transformation.format) {
46
+ case 'jpeg':
47
+ image = image.jpeg(options)
48
+ break
49
+ case 'png':
50
+ image = image.png(options)
51
+ break
52
+ case 'webp':
53
+ image = image.webp(options)
54
+ break
55
+ case 'avif':
56
+ image = image.avif(options)
57
+ break
58
+ }
59
+ }
60
+
61
+ return await image.toBuffer()
62
+ }
63
+
64
+ /**
65
+ * Processes all transformations for an image
66
+ * Uploads the original and all transformed versions
67
+ */
68
+ export async function processImageTransformations(
69
+ buffer: Buffer | Uint8Array,
70
+ originalFilename: string,
71
+ transformations: Record<string, ImageTransformationConfig>,
72
+ storageProvider: StorageProvider,
73
+ contentType: string,
74
+ ): Promise<Record<string, ImageTransformationResult>> {
75
+ const results: Record<string, ImageTransformationResult> = {}
76
+
77
+ for (const [name, config] of Object.entries(transformations)) {
78
+ // Transform the image
79
+ const transformedBuffer = await transformImage(buffer, config)
80
+
81
+ // Get dimensions of transformed image
82
+ const { width, height } = await getImageDimensions(transformedBuffer)
83
+
84
+ // Generate filename for transformation
85
+ const ext = config.format ? `.${config.format}` : ''
86
+ const transformedFilename = `${originalFilename}-${name}${ext}`
87
+
88
+ // Upload transformed image
89
+ const uploadResult = await storageProvider.upload(transformedBuffer, transformedFilename, {
90
+ contentType:
91
+ config.format === 'jpeg'
92
+ ? 'image/jpeg'
93
+ : config.format === 'png'
94
+ ? 'image/png'
95
+ : config.format === 'webp'
96
+ ? 'image/webp'
97
+ : config.format === 'avif'
98
+ ? 'image/avif'
99
+ : contentType,
100
+ })
101
+
102
+ results[name] = {
103
+ url: uploadResult.url,
104
+ width,
105
+ height,
106
+ size: uploadResult.size,
107
+ }
108
+ }
109
+
110
+ return results
111
+ }
@@ -0,0 +1,2 @@
1
+ export * from './image.js'
2
+ export * from './upload.js'
@@ -0,0 +1,122 @@
1
+ import mime from 'mime-types'
2
+
3
+ /**
4
+ * File validation options
5
+ */
6
+ export interface FileValidationOptions {
7
+ /** Maximum file size in bytes */
8
+ maxFileSize?: number
9
+ /** Accepted MIME types (e.g., ['image/jpeg', 'image/png']) */
10
+ acceptedMimeTypes?: string[]
11
+ /** Accepted file extensions (e.g., ['.jpg', '.png']) */
12
+ acceptedExtensions?: string[]
13
+ }
14
+
15
+ /**
16
+ * File validation result
17
+ */
18
+ export interface FileValidationResult {
19
+ valid: boolean
20
+ error?: string
21
+ }
22
+
23
+ /**
24
+ * Validates a file against the provided options
25
+ */
26
+ export function validateFile(
27
+ file: { size: number; name: string; type: string },
28
+ options?: FileValidationOptions,
29
+ ): FileValidationResult {
30
+ if (!options) {
31
+ return { valid: true }
32
+ }
33
+
34
+ // Check file size
35
+ if (options.maxFileSize && file.size > options.maxFileSize) {
36
+ return {
37
+ valid: false,
38
+ error: `File size exceeds maximum allowed size of ${formatFileSize(options.maxFileSize)}`,
39
+ }
40
+ }
41
+
42
+ // Check MIME type
43
+ if (options.acceptedMimeTypes && options.acceptedMimeTypes.length > 0) {
44
+ const fileMimeType = file.type || mime.lookup(file.name) || ''
45
+ if (!options.acceptedMimeTypes.includes(fileMimeType)) {
46
+ return {
47
+ valid: false,
48
+ error: `File type '${fileMimeType}' is not allowed. Accepted types: ${options.acceptedMimeTypes.join(', ')}`,
49
+ }
50
+ }
51
+ }
52
+
53
+ // Check file extension
54
+ if (options.acceptedExtensions && options.acceptedExtensions.length > 0) {
55
+ const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
56
+ if (!options.acceptedExtensions.includes(ext)) {
57
+ return {
58
+ valid: false,
59
+ error: `File extension '${ext}' is not allowed. Accepted extensions: ${options.acceptedExtensions.join(', ')}`,
60
+ }
61
+ }
62
+ }
63
+
64
+ return { valid: true }
65
+ }
66
+
67
+ /**
68
+ * Formats file size in human-readable format
69
+ */
70
+ export function formatFileSize(bytes: number): string {
71
+ if (bytes === 0) return '0 Bytes'
72
+
73
+ const k = 1024
74
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
75
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
76
+
77
+ return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`
78
+ }
79
+
80
+ /**
81
+ * Gets MIME type from filename
82
+ */
83
+ export function getMimeType(filename: string): string {
84
+ return mime.lookup(filename) || 'application/octet-stream'
85
+ }
86
+
87
+ /**
88
+ * Extracts file metadata from a File or Blob
89
+ */
90
+ export interface FileInfo {
91
+ name: string
92
+ size: number
93
+ type: string
94
+ lastModified?: number
95
+ }
96
+
97
+ /**
98
+ * Converts a File/Blob to Buffer for Node.js processing
99
+ */
100
+ export async function fileToBuffer(file: Blob | File): Promise<Buffer> {
101
+ const arrayBuffer = await file.arrayBuffer()
102
+ return Buffer.from(arrayBuffer)
103
+ }
104
+
105
+ /**
106
+ * Parses FormData and extracts file information
107
+ * This is a utility for developers to use in their upload routes
108
+ */
109
+ export async function parseFileFromFormData(
110
+ formData: FormData,
111
+ fieldName: string = 'file',
112
+ ): Promise<{ file: File; buffer: Buffer } | null> {
113
+ const file = formData.get(fieldName)
114
+
115
+ if (!file || !(file instanceof File)) {
116
+ return null
117
+ }
118
+
119
+ const buffer = await fileToBuffer(file)
120
+
121
+ return { file, buffer }
122
+ }