@adminforth/upload 1.4.7 → 1.5.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/build.log CHANGED
@@ -11,5 +11,5 @@ custom/preview.vue
11
11
  custom/tsconfig.json
12
12
  custom/uploader.vue
13
13
 
14
- sent 43,060 bytes received 134 bytes 86,388.00 bytes/sec
15
- total size is 42,578 speedup is 0.99
14
+ sent 43,165 bytes received 134 bytes 86,598.00 bytes/sec
15
+ total size is 42,676 speedup is 0.99
@@ -7,7 +7,6 @@
7
7
  class="rounded-md"
8
8
  :style="[maxWidth, minWidth]"
9
9
  ref="img"
10
- data-zoomable
11
10
  @click.stop="zoom.open()"
12
11
  />
13
12
  <video
@@ -229,8 +229,8 @@ const onFileChange = async (e) => {
229
229
  reader.readAsDataURL(file);
230
230
  }
231
231
 
232
- const { uploadUrl, tagline, s3Path, error } = await callAdminForthApi({
233
- path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
232
+ const { uploadUrl, uploadExtraParams, filePath, error } = await callAdminForthApi({
233
+ path: `/plugin/${props.meta.pluginInstanceId}/get_file_upload_url`,
234
234
  method: 'POST',
235
235
  body: {
236
236
  originalFilename: nameNoExtension,
@@ -266,7 +266,9 @@ const onFileChange = async (e) => {
266
266
  });
267
267
  xhr.open('PUT', uploadUrl, true);
268
268
  xhr.setRequestHeader('Content-Type', type);
269
- xhr.setRequestHeader('x-amz-tagging', tagline);
269
+ uploadExtraParams && Object.entries(uploadExtraParams).forEach(([key, value]: [string, string]) => {
270
+ xhr.setRequestHeader(key, value);
271
+ })
270
272
  xhr.send(file);
271
273
  });
272
274
  if (!success) {
@@ -284,7 +286,7 @@ const onFileChange = async (e) => {
284
286
  return;
285
287
  }
286
288
  uploaded.value = true;
287
- emit('update:value', s3Path);
289
+ emit('update:value', filePath);
288
290
  } catch (error) {
289
291
  console.error('Error uploading file:', error);
290
292
  adminforth.alert({
@@ -7,7 +7,6 @@
7
7
  class="rounded-md"
8
8
  :style="[maxWidth, minWidth]"
9
9
  ref="img"
10
- data-zoomable
11
10
  @click.stop="zoom.open()"
12
11
  />
13
12
  <video
@@ -229,8 +229,8 @@ const onFileChange = async (e) => {
229
229
  reader.readAsDataURL(file);
230
230
  }
231
231
 
232
- const { uploadUrl, tagline, s3Path, error } = await callAdminForthApi({
233
- path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
232
+ const { uploadUrl, uploadExtraParams, filePath, error } = await callAdminForthApi({
233
+ path: `/plugin/${props.meta.pluginInstanceId}/get_file_upload_url`,
234
234
  method: 'POST',
235
235
  body: {
236
236
  originalFilename: nameNoExtension,
@@ -266,7 +266,9 @@ const onFileChange = async (e) => {
266
266
  });
267
267
  xhr.open('PUT', uploadUrl, true);
268
268
  xhr.setRequestHeader('Content-Type', type);
269
- xhr.setRequestHeader('x-amz-tagging', tagline);
269
+ uploadExtraParams && Object.entries(uploadExtraParams).forEach(([key, value]: [string, string]) => {
270
+ xhr.setRequestHeader(key, value);
271
+ })
270
272
  xhr.send(file);
271
273
  });
272
274
  if (!success) {
@@ -284,7 +286,7 @@ const onFileChange = async (e) => {
284
286
  return;
285
287
  }
286
288
  uploaded.value = true;
287
- emit('update:value', s3Path);
289
+ emit('update:value', filePath);
288
290
  } catch (error) {
289
291
  console.error('Error uploading file:', error);
290
292
  adminforth.alert({
package/dist/index.js CHANGED
@@ -7,8 +7,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
11
- import { ExpirationStatus, GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
12
10
  import { AdminForthPlugin, Filters, suggestIfTypo } from "adminforth";
13
11
  import { Readable } from "stream";
14
12
  import { RateLimiter } from "adminforth";
@@ -26,74 +24,17 @@ export default class UploadPlugin extends AdminForthPlugin {
26
24
  }
27
25
  setupLifecycleRule() {
28
26
  return __awaiter(this, void 0, void 0, function* () {
29
- // check that lifecyle rule "adminforth-unused-cleaner" exists
30
- const CLEANUP_RULE_ID = 'adminforth-unused-cleaner';
31
- const s3 = new S3({
32
- credentials: {
33
- accessKeyId: this.options.s3AccessKeyId,
34
- secretAccessKey: this.options.s3SecretAccessKey,
35
- },
36
- region: this.options.s3Region,
37
- });
38
- // check bucket exists
39
- const bucketExists = s3.headBucket({ Bucket: this.options.s3Bucket });
40
- if (!bucketExists) {
41
- throw new Error(`Bucket ${this.options.s3Bucket} does not exist`);
42
- }
43
- // check that lifecycle rule exists
44
- let ruleExists = false;
45
- try {
46
- const lifecycleConfig = yield s3.getBucketLifecycleConfiguration({ Bucket: this.options.s3Bucket });
47
- ruleExists = lifecycleConfig.Rules.some((rule) => rule.ID === CLEANUP_RULE_ID);
48
- }
49
- catch (e) {
50
- if (e.name !== 'NoSuchLifecycleConfiguration') {
51
- console.error(`⛔ Error checking lifecycle configuration, please check keys have permissions to
52
- getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${this.options.s3Region}. Exception:`, e);
53
- throw e;
54
- }
55
- else {
56
- ruleExists = false;
57
- }
58
- }
59
- if (!ruleExists) {
60
- // create
61
- // rule deletes object has tag adminforth-candidate-for-cleanup = true after 2 days
62
- const params = {
63
- Bucket: this.options.s3Bucket,
64
- LifecycleConfiguration: {
65
- Rules: [
66
- {
67
- ID: CLEANUP_RULE_ID,
68
- Status: ExpirationStatus.Enabled,
69
- Filter: {
70
- Tag: {
71
- Key: ADMINFORTH_NOT_YET_USED_TAG,
72
- Value: 'true'
73
- }
74
- },
75
- Expiration: {
76
- Days: 2
77
- }
78
- }
79
- ]
80
- }
81
- };
82
- yield s3.putBucketLifecycleConfiguration(params);
83
- }
27
+ this.options.storage.adapter.setupLifecycle();
84
28
  });
85
29
  }
86
- genPreviewUrl(record, s3) {
30
+ genPreviewUrl(record) {
87
31
  return __awaiter(this, void 0, void 0, function* () {
88
32
  var _a;
89
33
  if ((_a = this.options.preview) === null || _a === void 0 ? void 0 : _a.previewUrl) {
90
- record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ s3Path: record[this.options.pathColumnName] });
34
+ record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ filePath: record[this.options.pathColumnName] });
91
35
  return;
92
36
  }
93
- const previewUrl = yield yield getSignedUrl(s3, new GetObjectCommand({
94
- Bucket: this.options.s3Bucket,
95
- Key: record[this.options.pathColumnName],
96
- }));
37
+ const previewUrl = yield this.options.storage.adapter.getDownloadUrl(record[this.options.pathColumnName], 1800);
97
38
  record[`previewUrl_${this.pluginInstanceId}`] = previewUrl;
98
39
  });
99
40
  }
@@ -205,22 +146,9 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
205
146
  resourceConfig.hooks.create.afterSave.push((_a) => __awaiter(this, [_a], void 0, function* ({ record }) {
206
147
  process.env.HEAVY_DEBUG && console.log('💾💾 after save ', record === null || record === void 0 ? void 0 : record.id);
207
148
  if (record[pathColumnName]) {
208
- const s3 = new S3({
209
- credentials: {
210
- accessKeyId: this.options.s3AccessKeyId,
211
- secretAccessKey: this.options.s3SecretAccessKey,
212
- },
213
- region: this.options.s3Region,
214
- });
215
149
  process.env.HEAVY_DEBUG && console.log('🪥🪥 remove ObjectTagging', record[pathColumnName]);
216
150
  // let it crash if it fails: this is a new file which just was uploaded.
217
- yield s3.putObjectTagging({
218
- Bucket: this.options.s3Bucket,
219
- Key: record[pathColumnName],
220
- Tagging: {
221
- TagSet: []
222
- }
223
- });
151
+ yield this.options.storage.adapter.markKeyForNotDeletation(record[pathColumnName]);
224
152
  }
225
153
  return { ok: true };
226
154
  }));
@@ -233,14 +161,7 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
233
161
  return { ok: true };
234
162
  }
235
163
  if (record[pathColumnName]) {
236
- const s3 = new S3({
237
- credentials: {
238
- accessKeyId: this.options.s3AccessKeyId,
239
- secretAccessKey: this.options.s3SecretAccessKey,
240
- },
241
- region: this.options.s3Region,
242
- });
243
- yield this.genPreviewUrl(record, s3);
164
+ yield this.genPreviewUrl(record);
244
165
  }
245
166
  return { ok: true };
246
167
  }));
@@ -248,16 +169,9 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
248
169
  // ** HOOKS FOR LIST **//
249
170
  if (pathColumn.showIn.list) {
250
171
  resourceConfig.hooks.list.afterDatasourceResponse.push((_a) => __awaiter(this, [_a], void 0, function* ({ response }) {
251
- const s3 = new S3({
252
- credentials: {
253
- accessKeyId: this.options.s3AccessKeyId,
254
- secretAccessKey: this.options.s3SecretAccessKey,
255
- },
256
- region: this.options.s3Region,
257
- });
258
172
  yield Promise.all(response.map((record) => __awaiter(this, void 0, void 0, function* () {
259
173
  if (record[this.options.pathColumnName]) {
260
- yield this.genPreviewUrl(record, s3);
174
+ yield this.genPreviewUrl(record);
261
175
  }
262
176
  })));
263
177
  return { ok: true };
@@ -267,26 +181,8 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
267
181
  // add delete hook which sets tag adminforth-candidate-for-cleanup to true
268
182
  resourceConfig.hooks.delete.afterSave.push((_a) => __awaiter(this, [_a], void 0, function* ({ record }) {
269
183
  if (record[pathColumnName]) {
270
- const s3 = new S3({
271
- credentials: {
272
- accessKeyId: this.options.s3AccessKeyId,
273
- secretAccessKey: this.options.s3SecretAccessKey,
274
- },
275
- region: this.options.s3Region,
276
- });
277
184
  try {
278
- yield s3.putObjectTagging({
279
- Bucket: this.options.s3Bucket,
280
- Key: record[pathColumnName],
281
- Tagging: {
282
- TagSet: [
283
- {
284
- Key: ADMINFORTH_NOT_YET_USED_TAG,
285
- Value: 'true'
286
- }
287
- ]
288
- }
289
- });
185
+ yield this.options.storage.adapter.markKeyForDeletation(record[pathColumnName]);
290
186
  }
291
187
  catch (e) {
292
188
  // file might be e.g. already deleted, so we catch error
@@ -307,28 +203,10 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
307
203
  // add edit postSave hook to delete old file and remove tag from new file
308
204
  resourceConfig.hooks.edit.afterSave.push((_a) => __awaiter(this, [_a], void 0, function* ({ updates, oldRecord }) {
309
205
  if (updates[virtualColumn.name] || updates[virtualColumn.name] === null) {
310
- const s3 = new S3({
311
- credentials: {
312
- accessKeyId: this.options.s3AccessKeyId,
313
- secretAccessKey: this.options.s3SecretAccessKey,
314
- },
315
- region: this.options.s3Region,
316
- });
317
206
  if (oldRecord[pathColumnName]) {
318
207
  // put tag to delete old file
319
208
  try {
320
- yield s3.putObjectTagging({
321
- Bucket: this.options.s3Bucket,
322
- Key: oldRecord[pathColumnName],
323
- Tagging: {
324
- TagSet: [
325
- {
326
- Key: ADMINFORTH_NOT_YET_USED_TAG,
327
- Value: 'true'
328
- }
329
- ]
330
- }
331
- });
209
+ yield this.options.storage.adapter.markKeyForDeletation(oldRecord[pathColumnName]);
332
210
  }
333
211
  catch (e) {
334
212
  // file might be e.g. already deleted, so we catch error
@@ -338,13 +216,7 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
338
216
  if (updates[virtualColumn.name] !== null) {
339
217
  // remove tag from new file
340
218
  // in this case we let it crash if it fails: this is a new file which just was uploaded.
341
- yield s3.putObjectTagging({
342
- Bucket: this.options.s3Bucket,
343
- Key: updates[pathColumnName],
344
- Tagging: {
345
- TagSet: []
346
- }
347
- });
219
+ yield this.options.storage.adapter.markKeyForNotDeletation(updates[pathColumnName]);
348
220
  }
349
221
  }
350
222
  return { ok: true };
@@ -370,7 +242,7 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
370
242
  });
371
243
  server.endpoint({
372
244
  method: 'POST',
373
- path: `/plugin/${this.pluginInstanceId}/get_s3_upload_url`,
245
+ path: `/plugin/${this.pluginInstanceId}/get_file_upload_url`,
374
246
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
375
247
  var _b, _c;
376
248
  const { originalFilename, contentType, size, originalExtension, recordPk } = body;
@@ -385,46 +257,22 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
385
257
  const pkName = (_b = this.resourceConfig.columns.find((column) => column.primaryKey)) === null || _b === void 0 ? void 0 : _b.name;
386
258
  record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(pkName, recordPk)]);
387
259
  }
388
- const s3Path = this.options.s3Path({ originalFilename, originalExtension, contentType, record });
389
- if (s3Path.startsWith('/')) {
260
+ const filePath = this.options.filePath({ originalFilename, originalExtension, contentType, record });
261
+ if (filePath.startsWith('/')) {
390
262
  throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
391
263
  }
392
- const s3 = new S3({
393
- credentials: {
394
- accessKeyId: this.options.s3AccessKeyId,
395
- secretAccessKey: this.options.s3SecretAccessKey,
396
- },
397
- region: this.options.s3Region,
398
- });
399
- const tagline = `${ADMINFORTH_NOT_YET_USED_TAG}=true`;
400
- const params = {
401
- Bucket: this.options.s3Bucket,
402
- Key: s3Path,
403
- ContentType: contentType,
404
- ACL: (this.options.s3ACL || 'private'),
405
- Tagging: tagline,
406
- };
407
- const uploadUrl = yield yield getSignedUrl(s3, new PutObjectCommand(params), {
408
- expiresIn: 1800,
409
- unhoistableHeaders: new Set(['x-amz-tagging']),
410
- });
264
+ const { uploadUrl, uploadExtraParams } = yield this.options.storage.adapter.getUploadSignedUrl(filePath, contentType, 1800);
411
265
  let previewUrl;
412
266
  if ((_c = this.options.preview) === null || _c === void 0 ? void 0 : _c.previewUrl) {
413
- previewUrl = this.options.preview.previewUrl({ s3Path });
414
- }
415
- else if (this.options.s3ACL === 'public-read') {
416
- previewUrl = `https://${this.options.s3Bucket}.s3.${this.options.s3Region}.amazonaws.com/${s3Path}`;
267
+ previewUrl = this.options.preview.previewUrl({ filePath });
417
268
  }
418
269
  else {
419
- previewUrl = yield getSignedUrl(s3, new GetObjectCommand({
420
- Bucket: this.options.s3Bucket,
421
- Key: s3Path,
422
- }));
270
+ previewUrl = yield this.options.storage.adapter.getDownloadUrl(filePath, 1800);
423
271
  }
424
272
  return {
425
273
  uploadUrl,
426
- s3Path,
427
- tagline,
274
+ filePath,
275
+ uploadExtraParams,
428
276
  previewUrl,
429
277
  };
430
278
  })
package/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
 
2
2
  import { PluginOptions } from './types.js';
3
- import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4
- import { ExpirationStatus, GetObjectCommand, ObjectCannedACL, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
5
3
  import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo } from "adminforth";
6
4
  import { Readable } from "stream";
7
5
  import { RateLimiter } from "adminforth";
@@ -30,76 +28,15 @@ export default class UploadPlugin extends AdminForthPlugin {
30
28
  }
31
29
 
32
30
  async setupLifecycleRule() {
33
- // check that lifecyle rule "adminforth-unused-cleaner" exists
34
- const CLEANUP_RULE_ID = 'adminforth-unused-cleaner';
35
-
36
- const s3 = new S3({
37
- credentials: {
38
- accessKeyId: this.options.s3AccessKeyId,
39
- secretAccessKey: this.options.s3SecretAccessKey,
40
- },
41
- region: this.options.s3Region,
42
- });
43
-
44
- // check bucket exists
45
- const bucketExists = s3.headBucket({ Bucket: this.options.s3Bucket })
46
- if (!bucketExists) {
47
- throw new Error(`Bucket ${this.options.s3Bucket} does not exist`);
48
- }
49
-
50
- // check that lifecycle rule exists
51
- let ruleExists: boolean = false;
52
-
53
- try {
54
- const lifecycleConfig: any = await s3.getBucketLifecycleConfiguration({ Bucket: this.options.s3Bucket });
55
- ruleExists = lifecycleConfig.Rules.some((rule: any) => rule.ID === CLEANUP_RULE_ID);
56
- } catch (e: any) {
57
- if (e.name !== 'NoSuchLifecycleConfiguration') {
58
- console.error(`⛔ Error checking lifecycle configuration, please check keys have permissions to
59
- getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${this.options.s3Region}. Exception:`, e);
60
- throw e;
61
- } else {
62
- ruleExists = false;
63
- }
64
- }
65
-
66
- if (!ruleExists) {
67
- // create
68
- // rule deletes object has tag adminforth-candidate-for-cleanup = true after 2 days
69
- const params = {
70
- Bucket: this.options.s3Bucket,
71
- LifecycleConfiguration: {
72
- Rules: [
73
- {
74
- ID: CLEANUP_RULE_ID,
75
- Status: ExpirationStatus.Enabled,
76
- Filter: {
77
- Tag: {
78
- Key: ADMINFORTH_NOT_YET_USED_TAG,
79
- Value: 'true'
80
- }
81
- },
82
- Expiration: {
83
- Days: 2
84
- }
85
- }
86
- ]
87
- }
88
- };
89
-
90
- await s3.putBucketLifecycleConfiguration(params);
91
- }
31
+ this.options.storage.adapter.setupLifecycle();
92
32
  }
93
33
 
94
- async genPreviewUrl(record: any, s3: S3) {
34
+ async genPreviewUrl(record: any) {
95
35
  if (this.options.preview?.previewUrl) {
96
- record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ s3Path: record[this.options.pathColumnName] });
36
+ record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ filePath: record[this.options.pathColumnName] });
97
37
  return;
98
38
  }
99
- const previewUrl = await await getSignedUrl(s3, new GetObjectCommand({
100
- Bucket: this.options.s3Bucket,
101
- Key: record[this.options.pathColumnName],
102
- }));
39
+ const previewUrl = await this.options.storage.adapter.getDownloadUrl(record[this.options.pathColumnName], 1800);
103
40
 
104
41
  record[`previewUrl_${this.pluginInstanceId}`] = previewUrl;
105
42
  }
@@ -222,23 +159,9 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
222
159
  process.env.HEAVY_DEBUG && console.log('💾💾 after save ', record?.id);
223
160
 
224
161
  if (record[pathColumnName]) {
225
- const s3 = new S3({
226
- credentials: {
227
- accessKeyId: this.options.s3AccessKeyId,
228
- secretAccessKey: this.options.s3SecretAccessKey,
229
- },
230
-
231
- region: this.options.s3Region,
232
- });
233
162
  process.env.HEAVY_DEBUG && console.log('🪥🪥 remove ObjectTagging', record[pathColumnName]);
234
163
  // let it crash if it fails: this is a new file which just was uploaded.
235
- await s3.putObjectTagging({
236
- Bucket: this.options.s3Bucket,
237
- Key: record[pathColumnName],
238
- Tagging: {
239
- TagSet: []
240
- }
241
- });
164
+ await this.options.storage.adapter.markKeyForNotDeletation(record[pathColumnName]);
242
165
  }
243
166
  return { ok: true };
244
167
  });
@@ -255,16 +178,7 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
255
178
  return { ok: true };
256
179
  }
257
180
  if (record[pathColumnName]) {
258
- const s3 = new S3({
259
- credentials: {
260
- accessKeyId: this.options.s3AccessKeyId,
261
- secretAccessKey: this.options.s3SecretAccessKey,
262
- },
263
-
264
- region: this.options.s3Region,
265
- });
266
-
267
- await this.genPreviewUrl(record, s3);
181
+ await this.genPreviewUrl(record)
268
182
  }
269
183
  return { ok: true };
270
184
  });
@@ -275,18 +189,9 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
275
189
 
276
190
  if (pathColumn.showIn.list) {
277
191
  resourceConfig.hooks.list.afterDatasourceResponse.push(async ({ response }: { response: any }) => {
278
- const s3 = new S3({
279
- credentials: {
280
- accessKeyId: this.options.s3AccessKeyId,
281
- secretAccessKey: this.options.s3SecretAccessKey,
282
- },
283
-
284
- region: this.options.s3Region,
285
- });
286
-
287
192
  await Promise.all(response.map(async (record: any) => {
288
193
  if (record[this.options.pathColumnName]) {
289
- await this.genPreviewUrl(record, s3);
194
+ await this.genPreviewUrl(record)
290
195
  }
291
196
  }));
292
197
  return { ok: true };
@@ -298,28 +203,8 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
298
203
  // add delete hook which sets tag adminforth-candidate-for-cleanup to true
299
204
  resourceConfig.hooks.delete.afterSave.push(async ({ record }: { record: any }) => {
300
205
  if (record[pathColumnName]) {
301
- const s3 = new S3({
302
- credentials: {
303
- accessKeyId: this.options.s3AccessKeyId,
304
- secretAccessKey: this.options.s3SecretAccessKey,
305
- },
306
-
307
- region: this.options.s3Region,
308
- });
309
-
310
206
  try {
311
- await s3.putObjectTagging({
312
- Bucket: this.options.s3Bucket,
313
- Key: record[pathColumnName],
314
- Tagging: {
315
- TagSet: [
316
- {
317
- Key: ADMINFORTH_NOT_YET_USED_TAG,
318
- Value: 'true'
319
- }
320
- ]
321
- }
322
- });
207
+ await this.options.storage.adapter.markKeyForDeletation(record[pathColumnName]);
323
208
  } catch (e) {
324
209
  // file might be e.g. already deleted, so we catch error
325
210
  console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${record[pathColumnName]}. File will not be auto-cleaned up`, e);
@@ -345,30 +230,10 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
345
230
  resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord: any }) => {
346
231
 
347
232
  if (updates[virtualColumn.name] || updates[virtualColumn.name] === null) {
348
- const s3 = new S3({
349
- credentials: {
350
- accessKeyId: this.options.s3AccessKeyId,
351
- secretAccessKey: this.options.s3SecretAccessKey,
352
- },
353
-
354
- region: this.options.s3Region,
355
- });
356
-
357
233
  if (oldRecord[pathColumnName]) {
358
234
  // put tag to delete old file
359
235
  try {
360
- await s3.putObjectTagging({
361
- Bucket: this.options.s3Bucket,
362
- Key: oldRecord[pathColumnName],
363
- Tagging: {
364
- TagSet: [
365
- {
366
- Key: ADMINFORTH_NOT_YET_USED_TAG,
367
- Value: 'true'
368
- }
369
- ]
370
- }
371
- });
236
+ await this.options.storage.adapter.markKeyForDeletation(oldRecord[pathColumnName]);
372
237
  } catch (e) {
373
238
  // file might be e.g. already deleted, so we catch error
374
239
  console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${oldRecord[pathColumnName]}. File will not be auto-cleaned up`, e);
@@ -377,13 +242,7 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
377
242
  if (updates[virtualColumn.name] !== null) {
378
243
  // remove tag from new file
379
244
  // in this case we let it crash if it fails: this is a new file which just was uploaded.
380
- await s3.putObjectTagging({
381
- Bucket: this.options.s3Bucket,
382
- Key: updates[pathColumnName],
383
- Tagging: {
384
- TagSet: []
385
- }
386
- });
245
+ await this.options.storage.adapter.markKeyForNotDeletation(updates[pathColumnName]);
387
246
  }
388
247
  }
389
248
  return { ok: true };
@@ -414,7 +273,7 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
414
273
 
415
274
  server.endpoint({
416
275
  method: 'POST',
417
- path: `/plugin/${this.pluginInstanceId}/get_s3_upload_url`,
276
+ path: `/plugin/${this.pluginInstanceId}/get_file_upload_url`,
418
277
  handler: async ({ body }) => {
419
278
  const { originalFilename, contentType, size, originalExtension, recordPk } = body;
420
279
 
@@ -433,49 +292,22 @@ getBucketLifecycleConfiguration on bucket ${this.options.s3Bucket} in region ${t
433
292
  )
434
293
  }
435
294
 
436
- const s3Path: string = this.options.s3Path({ originalFilename, originalExtension, contentType, record });
437
- if (s3Path.startsWith('/')) {
295
+ const filePath: string = this.options.filePath({ originalFilename, originalExtension, contentType, record });
296
+ if (filePath.startsWith('/')) {
438
297
  throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
439
298
  }
440
- const s3 = new S3({
441
- credentials: {
442
- accessKeyId: this.options.s3AccessKeyId,
443
- secretAccessKey: this.options.s3SecretAccessKey,
444
- },
445
-
446
- region: this.options.s3Region,
447
- });
448
-
449
- const tagline = `${ADMINFORTH_NOT_YET_USED_TAG}=true`;
450
- const params = {
451
- Bucket: this.options.s3Bucket,
452
- Key: s3Path,
453
- ContentType: contentType,
454
- ACL: (this.options.s3ACL || 'private') as ObjectCannedACL,
455
- Tagging: tagline,
456
- };
457
-
458
- const uploadUrl = await await getSignedUrl(s3, new PutObjectCommand(params), {
459
- expiresIn: 1800,
460
- unhoistableHeaders: new Set(['x-amz-tagging']),
461
- });
462
-
299
+ const { uploadUrl, uploadExtraParams } = await this.options.storage.adapter.getUploadSignedUrl(filePath, contentType, 1800);
463
300
  let previewUrl;
464
301
  if (this.options.preview?.previewUrl) {
465
- previewUrl = this.options.preview.previewUrl({ s3Path });
466
- } else if (this.options.s3ACL === 'public-read') {
467
- previewUrl = `https://${this.options.s3Bucket}.s3.${this.options.s3Region}.amazonaws.com/${s3Path}`;
302
+ previewUrl = this.options.preview.previewUrl({ filePath });
468
303
  } else {
469
- previewUrl = await getSignedUrl(s3, new GetObjectCommand({
470
- Bucket: this.options.s3Bucket,
471
- Key: s3Path,
472
- }));
304
+ previewUrl = await this.options.storage.adapter.getDownloadUrl(filePath, 1800);
473
305
  }
474
306
 
475
307
  return {
476
308
  uploadUrl,
477
- s3Path,
478
- tagline,
309
+ filePath,
310
+ uploadExtraParams,
479
311
  previewUrl,
480
312
  };
481
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "1.4.7",
3
+ "version": "1.5.0",
4
4
  "description": "Plugin for uploading files for adminforth",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdminUser, ImageGenerationAdapter } from "adminforth";
1
+ import { AdminUser, ImageGenerationAdapter, StorageAdapter } from "adminforth";
2
2
 
3
3
  export type PluginOptions = {
4
4
 
@@ -18,32 +18,6 @@ export type PluginOptions = {
18
18
  */
19
19
  maxFileSize?: number;
20
20
 
21
- /**
22
- * S3 bucket name where we will upload the files, e.g. 'my-bucket'
23
- */
24
- s3Bucket: string,
25
-
26
- /**
27
- * S3 region, e.g. 'us-east-1'
28
- */
29
- s3Region: string,
30
-
31
- /**
32
- * S3 access key id
33
- */
34
- s3AccessKeyId: string,
35
-
36
- /**
37
- * S3 secret access key
38
- */
39
- s3SecretAccessKey: string,
40
-
41
- /**
42
- * ACL which will be set to uploaded file, e.g. 'public-read'.
43
- * If you want to use 'public-read', it is your responsibility to set the "ACL Enabled" to true in the S3 bucket policy and Uncheck "Block all public access" in the bucket settings.
44
- */
45
- s3ACL?: string,
46
-
47
21
  /**
48
22
  * The path where the file will be uploaded to the S3 bucket, same path will be stored in the database
49
23
  * in the column specified in {@link pathColumnName}
@@ -55,7 +29,7 @@ export type PluginOptions = {
55
29
  * ```
56
30
  *
57
31
  */
58
- s3Path: ({originalFilename, originalExtension, contentType, record }: {
32
+ filePath: ({originalFilename, originalExtension, contentType, record }: {
59
33
  originalFilename: string,
60
34
  originalExtension: string,
61
35
  contentType: string,
@@ -113,7 +87,7 @@ export type PluginOptions = {
113
87
  * ```
114
88
  *
115
89
  */
116
- previewUrl?: ({s3Path}) => string,
90
+ previewUrl?: ({filePath}) => string,
117
91
  }
118
92
 
119
93
 
@@ -181,4 +155,12 @@ export type PluginOptions = {
181
155
 
182
156
  }
183
157
 
158
+ storage?: {
159
+ /**
160
+ * The adapter used to store the files.
161
+ * For now only S3 adapter is supported.
162
+ */
163
+ adapter: StorageAdapter,
164
+ }
165
+
184
166
  }