@startupjs-ui/file-input 0.1.5 → 0.1.9

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/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.1.9](https://github.com/startupjs/startupjs-ui/compare/v0.1.8...v0.1.9) (2026-01-16)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/file-input
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.8](https://github.com/startupjs/startupjs-ui/compare/v0.1.7...v0.1.8) (2026-01-08)
15
+
16
+ **Note:** Version bump only for package @startupjs-ui/file-input
17
+
18
+
19
+
20
+
21
+
6
22
  ## [0.1.5](https://github.com/startupjs/startupjs-ui/compare/v0.1.4...v0.1.5) (2025-12-29)
7
23
 
8
24
  **Note:** Version bump only for package @startupjs-ui/file-input
package/files.plugin.js CHANGED
@@ -3,7 +3,8 @@ import { createPlugin } from 'startupjs/registry'
3
3
  import busboy from 'busboy'
4
4
  import sharp from 'sharp'
5
5
  import { DELETE_FILE_URL, GET_FILE_URL, getDeleteFileUrl, getFileUrl, getUploadFileUrl, UPLOAD_SINGLE_FILE_URL } from './constants.js'
6
- import { deleteFile, getDefaultStorageType, getFileBlob, getFileSize, saveFileBlob } from './providers/index.js'
6
+ import { deleteFile, getDefaultStorageType, getFileBlob, getFileSize } from './providers/index.js'
7
+ import { uploadBuffer } from './server/index.js'
7
8
 
8
9
  export default createPlugin({
9
10
  name: 'files',
@@ -217,51 +218,19 @@ export default createPlugin({
217
218
  stream.on('data', data => buffers.push(data))
218
219
 
219
220
  stream.on('end', async () => {
220
- blob = Buffer.concat(buffers)
221
- meta = { filename, mimeType, encoding, storageType } // Update meta here to ensure it includes modifications for images
222
-
221
+ const blob = Buffer.concat(buffers)
222
+ meta = { filename, mimeType, encoding, storageType }
223
223
  if (!blob) return res.status(500).send('No file was uploaded')
224
224
 
225
225
  // extract extension from filename
226
226
  console.log('meta.filename', meta.filename)
227
227
  const extension = meta.filename?.match(/\.([^.]+)$/)?.[1]
228
228
  if (extension) meta.extension = extension
229
- const create = !fileId
230
- if (!fileId) fileId = $.id()
231
- // try to save file to sqlite first to do an early exit if it fails
232
229
  try {
233
- await saveFileBlob(storageType, fileId, blob)
230
+ fileId = await uploadBuffer(blob, { fileId, meta })
234
231
  } catch (err) {
235
232
  console.error(err)
236
- return res.status(500).send('Error saving file')
237
- }
238
- if (create) {
239
- const doc = { id: fileId, ...meta }
240
- // if some of the meta fields were undefined, remove them from the doc
241
- for (const key in meta) {
242
- if (meta[key] == null) delete doc[key]
243
- }
244
- await $.files.addNew(doc)
245
- } else {
246
- const $file = await sub($.files[fileId])
247
-
248
- // when changing storageType we should delete the file from the old storageType
249
- const oldStorageType = $file.storageType.get()
250
- if (oldStorageType !== meta.storageType) {
251
- try {
252
- await deleteFile(oldStorageType, fileId)
253
- } catch (err) {
254
- console.error(err)
255
- return res.status(500).send(`Error deleting file from old storageType ${oldStorageType}`)
256
- }
257
- }
258
-
259
- const doc = { ...$file.get(), ...meta, updatedAt: Date.now() }
260
- // if some of the meta fields were undefined, remove them from the doc
261
- for (const key in meta) {
262
- if (meta[key] == null) delete doc[key]
263
- }
264
- await $file.set(doc)
233
+ return res.status(500).send(err.message)
265
234
  }
266
235
  console.log(`Uploaded file to ${storageType}`, fileId)
267
236
  res.json({ fileId })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startupjs-ui/file-input",
3
- "version": "0.1.5",
3
+ "version": "0.1.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -9,6 +9,7 @@
9
9
  "type": "module",
10
10
  "exports": {
11
11
  ".": "./index.tsx",
12
+ "./server": "./server/index.js",
12
13
  "./constants": "./constants.js",
13
14
  "./deleteFile": "./deleteFile.ts",
14
15
  "./uploadFile": "./uploadFile.ts",
@@ -27,6 +28,7 @@
27
28
  "sharp": "^0.34.5"
28
29
  },
29
30
  "peerDependencies": {
31
+ "@aws-sdk/client-s3": "*",
30
32
  "@azure/storage-blob": "*",
31
33
  "mongodb": "*",
32
34
  "react": "*",
@@ -34,6 +36,9 @@
34
36
  "startupjs": "*"
35
37
  },
36
38
  "peerDependenciesMeta": {
39
+ "@aws-sdk/client-s3": {
40
+ "optional": true
41
+ },
37
42
  "@azure/storage-blob": {
38
43
  "optional": true
39
44
  },
@@ -41,5 +46,5 @@
41
46
  "optional": true
42
47
  }
43
48
  },
44
- "gitHead": "1b90893dc24a9b3ffde1284c58996b42e98913c6"
49
+ "gitHead": "215449157f986f8cd7139ebe27015db5f39da4ce"
45
50
  }
@@ -0,0 +1,241 @@
1
+ import { $, sub } from 'startupjs'
2
+ import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'
3
+
4
+ // AWS S3 Configuration from environment variables
5
+ const AWS_REGION = process.env.AWS_REGION
6
+ const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID
7
+ const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY
8
+ const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME
9
+ const AWS_S3_ENDPOINT = process.env.AWS_S3_ENDPOINT // Optional: for MinIO, LocalStack, etc.
10
+
11
+ let s3Client
12
+ let bucketName
13
+
14
+ export function validateSupport () {
15
+ if (!AWS_REGION) {
16
+ throw new Error(ERRORS.awsRegionNotAvailable)
17
+ }
18
+ if (!AWS_ACCESS_KEY_ID) {
19
+ throw new Error(ERRORS.awsAccessKeyNotAvailable)
20
+ }
21
+ if (!AWS_SECRET_ACCESS_KEY) {
22
+ throw new Error(ERRORS.awsSecretKeyNotAvailable)
23
+ }
24
+ if (!AWS_S3_BUCKET_NAME) {
25
+ throw new Error(ERRORS.awsBucketNotAvailable)
26
+ }
27
+
28
+ // Initialize the S3Client once
29
+ if (!s3Client) {
30
+ console.log('[@startupjs-ui] FileInput: Connecting to AWS S3', {
31
+ region: AWS_REGION,
32
+ bucket: AWS_S3_BUCKET_NAME,
33
+ endpoint: AWS_S3_ENDPOINT
34
+ })
35
+
36
+ const clientConfig = {
37
+ region: AWS_REGION,
38
+ credentials: {
39
+ accessKeyId: AWS_ACCESS_KEY_ID,
40
+ secretAccessKey: AWS_SECRET_ACCESS_KEY
41
+ }
42
+ }
43
+
44
+ // Support for custom endpoints (like MinIO, LocalStack, etc.)
45
+ if (AWS_S3_ENDPOINT) {
46
+ clientConfig.endpoint = AWS_S3_ENDPOINT
47
+ clientConfig.forcePathStyle = true
48
+ }
49
+
50
+ s3Client = new S3Client(clientConfig)
51
+ bucketName = AWS_S3_BUCKET_NAME
52
+ }
53
+ }
54
+
55
+ export async function getFileBlob (fileId, options = {}) {
56
+ validateSupport()
57
+
58
+ const { range } = options
59
+ const filePath = await getFilePath(fileId)
60
+ const params = {
61
+ Bucket: bucketName,
62
+ Key: filePath
63
+ }
64
+
65
+ try {
66
+ // Get file info first to validate range
67
+ const headCommand = new HeadObjectCommand(params)
68
+ const headResponse = await s3Client.send(headCommand)
69
+ const actualFileSize = headResponse.ContentLength
70
+
71
+ if (range) {
72
+ console.log('[AWS S3] Using Range request for optimal streaming:', { fileId, range, filePath })
73
+
74
+ // Validate range boundaries
75
+ if (range.start >= actualFileSize || range.start < 0) {
76
+ console.log('[AWS S3] Range start out of bounds:', { start: range.start, actualFileSize })
77
+ throw new Error('Range start out of bounds')
78
+ }
79
+
80
+ // Ensure end is within file bounds
81
+ const adjustedEnd = Math.min(range.end, actualFileSize - 1)
82
+
83
+ // Ensure end is not before start
84
+ if (adjustedEnd < range.start) {
85
+ console.log('[AWS S3] Invalid range:', { start: range.start, end: adjustedEnd, actualFileSize })
86
+ throw new Error('Invalid range')
87
+ }
88
+
89
+ console.log('[AWS S3] Downloading object with range:', {
90
+ start: range.start,
91
+ end: adjustedEnd,
92
+ actualFileSize,
93
+ originalEnd: range.end
94
+ })
95
+
96
+ // Download with range
97
+ params.Range = `bytes=${range.start}-${adjustedEnd}`
98
+ const command = new GetObjectCommand(params)
99
+ const response = await s3Client.send(command)
100
+
101
+ // Convert stream to buffer
102
+ const chunks = []
103
+ for await (const chunk of response.Body) {
104
+ chunks.push(chunk)
105
+ }
106
+
107
+ const result = Buffer.concat(chunks)
108
+ const expectedSize = adjustedEnd - range.start + 1
109
+
110
+ console.log('[AWS S3] Range response:', {
111
+ expected: expectedSize,
112
+ actual: result.length,
113
+ start: range.start,
114
+ end: adjustedEnd,
115
+ fileId,
116
+ filePath
117
+ })
118
+
119
+ if (result.length === 0) {
120
+ console.warn('[AWS S3] Empty range response - this may indicate a problem')
121
+ }
122
+
123
+ return result
124
+ } else {
125
+ // Regular download for non-Range requests
126
+ const command = new GetObjectCommand(params)
127
+ const response = await s3Client.send(command)
128
+
129
+ const chunks = []
130
+ for await (const chunk of response.Body) {
131
+ chunks.push(chunk)
132
+ }
133
+
134
+ return Buffer.concat(chunks)
135
+ }
136
+ } catch (error) {
137
+ if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
138
+ throw new Error(ERRORS.fileNotFound)
139
+ }
140
+ console.error('[AWS S3] Error downloading object:', error)
141
+ throw error
142
+ }
143
+ }
144
+
145
+ export async function getFileSize (fileId, options) {
146
+ validateSupport()
147
+
148
+ const filePath = await getFilePath(fileId)
149
+ const params = {
150
+ Bucket: bucketName,
151
+ Key: filePath
152
+ }
153
+
154
+ try {
155
+ const command = new HeadObjectCommand(params)
156
+ const response = await s3Client.send(command)
157
+ return response.ContentLength
158
+ } catch (error) {
159
+ if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
160
+ throw new Error(ERRORS.fileNotFound)
161
+ }
162
+ console.error('[AWS S3] Error getting object size:', error)
163
+ throw error
164
+ }
165
+ }
166
+
167
+ export async function saveFileBlob (fileId, blob, options = {}) {
168
+ validateSupport()
169
+ const filePath = generateFilePath(fileId, options)
170
+ const params = {
171
+ Bucket: bucketName,
172
+ Key: filePath,
173
+ Body: blob
174
+ }
175
+
176
+ try {
177
+ // Upload object (overwrites if exists)
178
+ const command = new PutObjectCommand(params)
179
+ await s3Client.send(command)
180
+ console.log('[AWS S3] Object uploaded successfully:', filePath)
181
+ } catch (error) {
182
+ console.error('[AWS S3] Error uploading object:', error)
183
+ throw error
184
+ }
185
+ }
186
+
187
+ export async function deleteFile (fileId, options) {
188
+ validateSupport()
189
+
190
+ const filePath = await getFilePath(fileId)
191
+ const params = {
192
+ Bucket: bucketName,
193
+ Key: filePath
194
+ }
195
+
196
+ try {
197
+ const command = new DeleteObjectCommand(params)
198
+ await s3Client.send(command)
199
+ console.log('[AWS S3] Object deleted successfully:', filePath)
200
+ } catch (error) {
201
+ if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
202
+ throw new Error(ERRORS.fileNotFound)
203
+ }
204
+ console.error('[AWS S3] Error deleting object:', error)
205
+ throw error
206
+ }
207
+ }
208
+
209
+ async function getFilePath (fileId) {
210
+ const $file = await sub($.files[fileId])
211
+ const file = $file.get()
212
+ return generateFilePath(fileId, file)
213
+ }
214
+
215
+ function generateFilePath (fileId, options) {
216
+ const { filename, path, setUniqName } = options
217
+ const newFilename = setUniqName ? fileId : filename || fileId
218
+ return path ? path + newFilename : newFilename
219
+ }
220
+
221
+ const ERRORS = {
222
+ awsRegionNotAvailable: `
223
+ AWS S3 region is not available.
224
+ Make sure you have configured the AWS_REGION environment variable.
225
+ `,
226
+ awsAccessKeyNotAvailable: `
227
+ AWS access key ID is not available.
228
+ Make sure you have configured the AWS_ACCESS_KEY_ID environment variable.
229
+ `,
230
+ awsSecretKeyNotAvailable: `
231
+ AWS secret access key is not available.
232
+ Make sure you have configured the AWS_SECRET_ACCESS_KEY environment variable.
233
+ `,
234
+ awsBucketNotAvailable: `
235
+ AWS S3 bucket name is not available.
236
+ Make sure you have configured the AWS_S3_BUCKET_NAME environment variable.
237
+ `,
238
+ fileNotFound: `
239
+ File not found in AWS S3.
240
+ `
241
+ }
@@ -23,8 +23,9 @@ export function validateSupport () {
23
23
  }
24
24
  }
25
25
 
26
- export async function getFileBlob (fileId, range) {
26
+ export async function getFileBlob (fileId, options = {}) {
27
27
  validateSupport()
28
+ const { range } = options
28
29
  const blobClient = containerClient.getBlobClient(fileId)
29
30
 
30
31
  try {
@@ -101,7 +102,7 @@ export async function getFileBlob (fileId, range) {
101
102
  }
102
103
  }
103
104
 
104
- export async function getFileSize (fileId) {
105
+ export async function getFileSize (fileId, options) {
105
106
  validateSupport()
106
107
  const blobClient = containerClient.getBlobClient(fileId)
107
108
 
@@ -117,7 +118,7 @@ export async function getFileSize (fileId) {
117
118
  }
118
119
  }
119
120
 
120
- export async function saveFileBlob (fileId, blob) {
121
+ export async function saveFileBlob (fileId, blob, options) {
121
122
  validateSupport()
122
123
  const blobClient = containerClient.getBlockBlobClient(fileId)
123
124
 
@@ -131,7 +132,7 @@ export async function saveFileBlob (fileId, blob) {
131
132
  }
132
133
  }
133
134
 
134
- export async function deleteFile (fileId) {
135
+ export async function deleteFile (fileId, options) {
135
136
  validateSupport()
136
137
  const blobClient = containerClient.getBlobClient(fileId)
137
138
 
@@ -1,19 +1,19 @@
1
1
  import { mongo, sqlite } from 'startupjs/server'
2
2
 
3
- export async function getFileBlob (storageType, fileId, range) {
4
- return (await getStorageProvider(storageType)).getFileBlob(fileId, range)
3
+ export async function getFileBlob (storageType, fileId, options) {
4
+ return (await getStorageProvider(storageType)).getFileBlob(fileId, options)
5
5
  }
6
6
 
7
- export async function getFileSize (storageType, fileId) {
8
- return (await getStorageProvider(storageType)).getFileSize(fileId)
7
+ export async function getFileSize (storageType, fileId, options) {
8
+ return (await getStorageProvider(storageType)).getFileSize(fileId, options)
9
9
  }
10
10
 
11
- export async function saveFileBlob (storageType, fileId, blob) {
12
- return (await getStorageProvider(storageType)).saveFileBlob(fileId, blob)
11
+ export async function saveFileBlob (storageType, fileId, blob, options) {
12
+ return (await getStorageProvider(storageType)).saveFileBlob(fileId, blob, options)
13
13
  }
14
14
 
15
- export async function deleteFile (storageType, fileId) {
16
- return (await getStorageProvider(storageType)).deleteFile(fileId)
15
+ export async function deleteFile (storageType, fileId, options) {
16
+ return (await getStorageProvider(storageType)).deleteFile(fileId, options)
17
17
  }
18
18
 
19
19
  export async function getDefaultStorageType () {
@@ -37,6 +37,8 @@ async function getStorageProvider (storageType) {
37
37
  theModule = await import('./mongo.js')
38
38
  } else if (storageType === 'azureblob') {
39
39
  theModule = await import('./azureblob.js')
40
+ } else if (storageType === 's3') {
41
+ theModule = await import('./awsS3.js')
40
42
  } else {
41
43
  throw Error(ERRORS.unsupportedStorageType(storageType))
42
44
  }
@@ -11,8 +11,9 @@ export function validateSupport () {
11
11
  }
12
12
  }
13
13
 
14
- export async function getFileBlob (fileId, range) {
14
+ export async function getFileBlob (fileId, options = {}) {
15
15
  validateSupport()
16
+ const { range } = options
16
17
  const files = await bucket.find({ filename: fileId }).toArray()
17
18
  if (!files || files.length === 0) {
18
19
  throw new Error(ERRORS.fileNotFound)
@@ -94,7 +95,7 @@ export async function getFileBlob (fileId, range) {
94
95
  })
95
96
  }
96
97
 
97
- export async function getFileSize (fileId) {
98
+ export async function getFileSize (fileId, options) {
98
99
  validateSupport()
99
100
  const files = await bucket.find({ filename: fileId }).toArray()
100
101
  if (!files || files.length === 0) {
@@ -103,7 +104,7 @@ export async function getFileSize (fileId) {
103
104
  return files[0].length
104
105
  }
105
106
 
106
- export async function saveFileBlob (fileId, blob) {
107
+ export async function saveFileBlob (fileId, blob, options) {
107
108
  console.log('[MongoDB GridFS] Saving file:', {
108
109
  fileId,
109
110
  blobType: blob ? blob.constructor.name : 'undefined',
@@ -164,7 +165,7 @@ export async function saveFileBlob (fileId, blob) {
164
165
  })
165
166
  }
166
167
 
167
- export async function deleteFile (fileId) {
168
+ export async function deleteFile (fileId, options) {
168
169
  validateSupport()
169
170
  const files = await bucket.find({ filename: fileId }).toArray()
170
171
  if (!files || files.length === 0) {
@@ -4,7 +4,8 @@ export async function validateSupport () {
4
4
  if (!sqlite) throw Error(ERRORS.disabled)
5
5
  }
6
6
 
7
- export async function getFileBlob (fileId, range) {
7
+ export async function getFileBlob (fileId, options = {}) {
8
+ const { range } = options
8
9
  return await new Promise((resolve, reject) => {
9
10
  sqlite.get('SELECT * FROM files WHERE id = ?', [fileId], (err, row) => {
10
11
  if (err) return reject(err)
@@ -64,7 +65,7 @@ export async function getFileBlob (fileId, range) {
64
65
  })
65
66
  }
66
67
 
67
- export async function saveFileBlob (fileId, blob) {
68
+ export async function saveFileBlob (fileId, blob, options) {
68
69
  return await new Promise((resolve, reject) => {
69
70
  sqlite.run('INSERT OR REPLACE INTO files (id, data) VALUES (?, ?)', [fileId, blob], err => {
70
71
  if (err) return reject(err)
@@ -73,7 +74,7 @@ export async function saveFileBlob (fileId, blob) {
73
74
  })
74
75
  }
75
76
 
76
- export async function deleteFile (fileId) {
77
+ export async function deleteFile (fileId, options) {
77
78
  return await new Promise((resolve, reject) => {
78
79
  sqlite.run('DELETE FROM files WHERE id = ?', [fileId], err => {
79
80
  if (err) return reject(err)
@@ -82,7 +83,7 @@ export async function deleteFile (fileId) {
82
83
  })
83
84
  }
84
85
 
85
- export async function getFileSize (fileId) {
86
+ export async function getFileSize (fileId, options) {
86
87
  await validateSupport()
87
88
  return await new Promise((resolve, reject) => {
88
89
  sqlite.get('SELECT data FROM files WHERE id = ?', [fileId], (err, row) => {
@@ -0,0 +1 @@
1
+ export { default as uploadBuffer } from './uploadBuffer.js'
@@ -0,0 +1,55 @@
1
+ import { $, sub } from 'startupjs'
2
+ import { deleteFile, getDefaultStorageType, saveFileBlob } from '../providers/index.js'
3
+
4
+ export default async function uploadBuffer (buff, options = {}) {
5
+ let { fileId, meta = {} } = options
6
+
7
+ let storageType = meta.storageType
8
+ try {
9
+ storageType ??= await getDefaultStorageType()
10
+ } catch (err) {
11
+ console.error(err)
12
+ throw new Error('Error getting default storage type')
13
+ }
14
+
15
+ const create = !fileId
16
+ if (!fileId) fileId = $.id()
17
+
18
+ // try to save file to sqlite first to do an early exit if it fails
19
+ try {
20
+ await saveFileBlob(storageType, fileId, buff, meta)
21
+ } catch (err) {
22
+ console.error(err)
23
+ throw new Error('Error saving file')
24
+ }
25
+
26
+ if (create) {
27
+ const doc = { id: fileId, ...meta, storageType }
28
+ // if some of the meta fields were undefined, remove them from the doc
29
+ for (const key in meta) {
30
+ if (meta[key] == null) delete doc[key]
31
+ }
32
+ await $.files.addNew(doc)
33
+ } else {
34
+ const $file = await sub($.files[fileId])
35
+
36
+ // when changing storageType we should delete the file from the old storageType
37
+ const oldStorageType = $file.storageType.get()
38
+ if (oldStorageType !== storageType) {
39
+ try {
40
+ await deleteFile(oldStorageType, fileId)
41
+ } catch (err) {
42
+ console.error(err)
43
+ throw new Error(`Error deleting file from old storageType ${oldStorageType}`)
44
+ }
45
+ }
46
+
47
+ const doc = { ...$file.get(), ...meta, storageType, updatedAt: Date.now() }
48
+ // if some of the meta fields were undefined, remove them from the doc
49
+ for (const key in meta) {
50
+ if (meta[key] == null) delete doc[key]
51
+ }
52
+ await $file.set(doc)
53
+ }
54
+ return fileId
55
+ }