@adminforth/upload 1.0.12 → 1.0.14

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.
@@ -18,7 +18,7 @@
18
18
  </video>
19
19
 
20
20
  <a v-else :href="url" target="_blank"
21
- class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
21
+ class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
22
22
  >
23
23
  <!-- download file icon -->
24
24
  <svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@@ -171,7 +171,7 @@ const onFileChange = async (e) => {
171
171
  reader.readAsDataURL(file);
172
172
  }
173
173
 
174
- const { uploadUrl, s3Path } = await callAdminForthApi({
174
+ const { uploadUrl, tagline, s3Path, error } = await callAdminForthApi({
175
175
  path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
176
176
  method: 'POST',
177
177
  body: {
@@ -182,6 +182,17 @@ const onFileChange = async (e) => {
182
182
  },
183
183
  });
184
184
 
185
+ if (error) {
186
+ window.adminforth.alert({
187
+ message: `File was not uploaded because of error: ${error}`,
188
+ variant: 'danger'
189
+ });
190
+ imgPreview.value = null;
191
+ uploaded.value = false;
192
+ progress.value = 0;
193
+ return;
194
+ }
195
+
185
196
  const xhr = new XMLHttpRequest();
186
197
  const success = await new Promise((resolve) => {
187
198
  xhr.upload.onprogress = (e) => {
@@ -196,7 +207,7 @@ const onFileChange = async (e) => {
196
207
  });
197
208
  xhr.open('PUT', uploadUrl, true);
198
209
  xhr.setRequestHeader('Content-Type', type);
199
- xhr.setRequestHeader('x-amz-tagging', (new URL(uploadUrl)).searchParams.get('x-amz-tagging'));
210
+ xhr.setRequestHeader('x-amz-tagging', tagline);
200
211
  xhr.send(file);
201
212
  });
202
213
  if (!success) {
@@ -18,7 +18,7 @@
18
18
  </video>
19
19
 
20
20
  <a v-else :href="url" target="_blank"
21
- class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
21
+ class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
22
22
  >
23
23
  <!-- download file icon -->
24
24
  <svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@@ -171,7 +171,7 @@ const onFileChange = async (e) => {
171
171
  reader.readAsDataURL(file);
172
172
  }
173
173
 
174
- const { uploadUrl, s3Path } = await callAdminForthApi({
174
+ const { uploadUrl, tagline, s3Path, error } = await callAdminForthApi({
175
175
  path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
176
176
  method: 'POST',
177
177
  body: {
@@ -182,6 +182,17 @@ const onFileChange = async (e) => {
182
182
  },
183
183
  });
184
184
 
185
+ if (error) {
186
+ window.adminforth.alert({
187
+ message: `File was not uploaded because of error: ${error}`,
188
+ variant: 'danger'
189
+ });
190
+ imgPreview.value = null;
191
+ uploaded.value = false;
192
+ progress.value = 0;
193
+ return;
194
+ }
195
+
185
196
  const xhr = new XMLHttpRequest();
186
197
  const success = await new Promise((resolve) => {
187
198
  xhr.upload.onprogress = (e) => {
@@ -196,7 +207,7 @@ const onFileChange = async (e) => {
196
207
  });
197
208
  xhr.open('PUT', uploadUrl, true);
198
209
  xhr.setRequestHeader('Content-Type', type);
199
- xhr.setRequestHeader('x-amz-tagging', (new URL(uploadUrl)).searchParams.get('x-amz-tagging'));
210
+ xhr.setRequestHeader('x-amz-tagging', tagline);
200
211
  xhr.send(file);
201
212
  });
202
213
  if (!success) {
package/dist/index.js CHANGED
@@ -7,8 +7,9 @@ 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 AWS from 'aws-sdk';
11
- import { AdminForthPlugin, AdminForthResourcePages } from "adminforth";
10
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
11
+ import { ExpirationStatus, GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
12
+ import { AdminForthPlugin } from "adminforth";
12
13
  const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup';
13
14
  export default class UploadPlugin extends AdminForthPlugin {
14
15
  constructor(options) {
@@ -22,24 +23,26 @@ export default class UploadPlugin extends AdminForthPlugin {
22
23
  return __awaiter(this, void 0, void 0, function* () {
23
24
  // check that lifecyle rule "adminforth-unused-cleaner" exists
24
25
  const CLEANUP_RULE_ID = 'adminforth-unused-cleaner';
25
- const s3 = new AWS.S3({
26
- accessKeyId: this.options.s3AccessKeyId,
27
- secretAccessKey: this.options.s3SecretAccessKey,
28
- region: this.options.s3Region
26
+ const s3 = new S3({
27
+ credentials: {
28
+ accessKeyId: this.options.s3AccessKeyId,
29
+ secretAccessKey: this.options.s3SecretAccessKey,
30
+ },
31
+ region: this.options.s3Region,
29
32
  });
30
33
  // check bucket exists
31
- const bucketExists = s3.headBucket({ Bucket: this.options.s3Bucket }).promise();
34
+ const bucketExists = s3.headBucket({ Bucket: this.options.s3Bucket });
32
35
  if (!bucketExists) {
33
36
  throw new Error(`Bucket ${this.options.s3Bucket} does not exist`);
34
37
  }
35
38
  // check that lifecycle rule exists
36
39
  let ruleExists = false;
37
40
  try {
38
- const lifecycleConfig = yield s3.getBucketLifecycleConfiguration({ Bucket: this.options.s3Bucket }).promise();
41
+ const lifecycleConfig = yield s3.getBucketLifecycleConfiguration({ Bucket: this.options.s3Bucket });
39
42
  ruleExists = lifecycleConfig.Rules.some((rule) => rule.ID === CLEANUP_RULE_ID);
40
43
  }
41
44
  catch (e) {
42
- if (e.code !== 'NoSuchLifecycleConfiguration') {
45
+ if (e.name !== 'NoSuchLifecycleConfiguration') {
43
46
  throw e;
44
47
  }
45
48
  else {
@@ -55,7 +58,7 @@ export default class UploadPlugin extends AdminForthPlugin {
55
58
  Rules: [
56
59
  {
57
60
  ID: CLEANUP_RULE_ID,
58
- Status: 'Enabled',
61
+ Status: ExpirationStatus.Enabled,
59
62
  Filter: {
60
63
  Tag: {
61
64
  Key: ADMINFORTH_NOT_YET_USED_TAG,
@@ -69,7 +72,7 @@ export default class UploadPlugin extends AdminForthPlugin {
69
72
  ]
70
73
  }
71
74
  };
72
- yield s3.putBucketLifecycleConfiguration(params).promise();
75
+ yield s3.putBucketLifecycleConfiguration(params);
73
76
  }
74
77
  });
75
78
  }
@@ -80,10 +83,10 @@ export default class UploadPlugin extends AdminForthPlugin {
80
83
  record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ s3Path: record[this.options.pathColumnName] });
81
84
  return;
82
85
  }
83
- const previewUrl = yield s3.getSignedUrl('getObject', {
86
+ const previewUrl = yield yield getSignedUrl(s3, new GetObjectCommand({
84
87
  Bucket: this.options.s3Bucket,
85
88
  Key: record[this.options.pathColumnName],
86
- });
89
+ }));
87
90
  record[`previewUrl_${this.pluginInstanceId}`] = previewUrl;
88
91
  });
89
92
  }
@@ -92,7 +95,7 @@ export default class UploadPlugin extends AdminForthPlugin {
92
95
  modifyResourceConfig: { get: () => super.modifyResourceConfig }
93
96
  });
94
97
  return __awaiter(this, void 0, void 0, function* () {
95
- var _a, _b, _c;
98
+ var _a, _b, _c, _d, _e;
96
99
  _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
97
100
  // after column to store the path of the uploaded file, add new VirtualColumn,
98
101
  // show only in edit and create views
@@ -106,29 +109,38 @@ export default class UploadPlugin extends AdminForthPlugin {
106
109
  const virtualColumn = {
107
110
  virtual: true,
108
111
  name: `uploader_${this.pluginInstanceId}`,
109
- components: Object.assign({ edit: {
112
+ components: {
113
+ edit: {
110
114
  file: this.componentPath('uploader.vue'),
111
115
  meta: pluginFrontendOptions,
112
- }, create: {
116
+ },
117
+ create: {
113
118
  file: this.componentPath('uploader.vue'),
114
119
  meta: pluginFrontendOptions,
115
- }, show: {
116
- file: this.componentPath('preview.vue'),
117
- meta: pluginFrontendOptions,
118
- } }, (((_a = this.options.preview) === null || _a === void 0 ? void 0 : _a.showInList) ? {
119
- list: {
120
- file: this.componentPath('preview.vue'),
121
- meta: pluginFrontendOptions,
122
- }
123
- } : {})),
124
- showIn: ['edit', 'create', 'show', ...(((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.showInList) ? [
125
- AdminForthResourcePages.list
126
- ] : [])],
120
+ },
121
+ },
122
+ showIn: ['edit', 'create'],
127
123
  };
128
124
  const pathColumnIndex = resourceConfig.columns.findIndex((column) => column.name === pathColumnName);
129
125
  if (pathColumnIndex === -1) {
130
126
  throw new Error(`Column with name "${pathColumnName}" not found in resource "${resourceConfig.name}"`);
131
127
  }
128
+ if (!resourceConfig.columns[pathColumnIndex].components) {
129
+ resourceConfig.columns[pathColumnIndex].components = {};
130
+ }
131
+ if (((_a = this.options.preview) === null || _a === void 0 ? void 0 : _a.showInList) || ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.showInList) === undefined) {
132
+ // add preview column to list
133
+ resourceConfig.columns[pathColumnIndex].components.list = {
134
+ file: this.componentPath('preview.vue'),
135
+ meta: pluginFrontendOptions,
136
+ };
137
+ }
138
+ if (((_c = this.options.preview) === null || _c === void 0 ? void 0 : _c.showInShow) || ((_d = this.options.preview) === null || _d === void 0 ? void 0 : _d.showInShow) === undefined) {
139
+ resourceConfig.columns[pathColumnIndex].components.show = {
140
+ file: this.componentPath('preview.vue'),
141
+ meta: pluginFrontendOptions,
142
+ };
143
+ }
132
144
  // insert virtual column after path column if it is not already there
133
145
  const virtualColumnIndex = resourceConfig.columns.findIndex((column) => column.name === virtualColumn.name);
134
146
  if (virtualColumnIndex === -1) {
@@ -144,7 +156,7 @@ export default class UploadPlugin extends AdminForthPlugin {
144
156
  virtualColumn.editingNote = pathColumn.editingNote;
145
157
  // ** HOOKS FOR CREATE **//
146
158
  // add beforeSave hook to save virtual column to path column
147
- resourceConfig.hooks.create.beforeSave.push((_d) => __awaiter(this, [_d], void 0, function* ({ record }) {
159
+ resourceConfig.hooks.create.beforeSave.push((_f) => __awaiter(this, [_f], void 0, function* ({ record }) {
148
160
  if (record[virtualColumn.name]) {
149
161
  record[pathColumnName] = record[virtualColumn.name];
150
162
  delete record[virtualColumn.name];
@@ -152,47 +164,56 @@ export default class UploadPlugin extends AdminForthPlugin {
152
164
  return { ok: true };
153
165
  }));
154
166
  // in afterSave hook, aremove tag adminforth-not-yet-used from the file
155
- resourceConfig.hooks.create.afterSave.push((_e) => __awaiter(this, [_e], void 0, function* ({ record }) {
167
+ resourceConfig.hooks.create.afterSave.push((_g) => __awaiter(this, [_g], void 0, function* ({ record }) {
168
+ process.env.HEAVY_DEBUG && console.log('💾💾 after save ', record === null || record === void 0 ? void 0 : record.id);
156
169
  if (record[pathColumnName]) {
157
- const s3 = new AWS.S3({
158
- accessKeyId: this.options.s3AccessKeyId,
159
- secretAccessKey: this.options.s3SecretAccessKey,
160
- region: this.options.s3Region
170
+ const s3 = new S3({
171
+ credentials: {
172
+ accessKeyId: this.options.s3AccessKeyId,
173
+ secretAccessKey: this.options.s3SecretAccessKey,
174
+ },
175
+ region: this.options.s3Region,
161
176
  });
177
+ process.env.HEAVY_DEBUG && console.log('🪥🪥 remove ObjectTagging', record[pathColumnName]);
178
+ // let it crash if it fails: this is a new file which just was uploaded.
162
179
  yield s3.putObjectTagging({
163
180
  Bucket: this.options.s3Bucket,
164
181
  Key: record[pathColumnName],
165
182
  Tagging: {
166
183
  TagSet: []
167
184
  }
168
- }).promise();
185
+ });
169
186
  }
170
187
  return { ok: true };
171
188
  }));
172
189
  // ** HOOKS FOR SHOW **//
173
190
  // add show hook to get presigned URL
174
- resourceConfig.hooks.show.afterDatasourceResponse.push((_f) => __awaiter(this, [_f], void 0, function* ({ response }) {
191
+ resourceConfig.hooks.show.afterDatasourceResponse.push((_h) => __awaiter(this, [_h], void 0, function* ({ response }) {
175
192
  const record = response[0];
176
193
  if (!record) {
177
194
  return { ok: true };
178
195
  }
179
196
  if (record[pathColumnName]) {
180
- const s3 = new AWS.S3({
181
- accessKeyId: this.options.s3AccessKeyId,
182
- secretAccessKey: this.options.s3SecretAccessKey,
183
- region: this.options.s3Region
197
+ const s3 = new S3({
198
+ credentials: {
199
+ accessKeyId: this.options.s3AccessKeyId,
200
+ secretAccessKey: this.options.s3SecretAccessKey,
201
+ },
202
+ region: this.options.s3Region,
184
203
  });
185
204
  yield this.genPreviewUrl(record, s3);
186
205
  }
187
206
  return { ok: true };
188
207
  }));
189
208
  // ** HOOKS FOR LIST **//
190
- if ((_c = this.options.preview) === null || _c === void 0 ? void 0 : _c.showInList) {
191
- resourceConfig.hooks.list.afterDatasourceResponse.push((_g) => __awaiter(this, [_g], void 0, function* ({ response }) {
192
- const s3 = new AWS.S3({
193
- accessKeyId: this.options.s3AccessKeyId,
194
- secretAccessKey: this.options.s3SecretAccessKey,
195
- region: this.options.s3Region
209
+ if ((_e = this.options.preview) === null || _e === void 0 ? void 0 : _e.showInList) {
210
+ resourceConfig.hooks.list.afterDatasourceResponse.push((_j) => __awaiter(this, [_j], void 0, function* ({ response }) {
211
+ const s3 = new S3({
212
+ credentials: {
213
+ accessKeyId: this.options.s3AccessKeyId,
214
+ secretAccessKey: this.options.s3SecretAccessKey,
215
+ },
216
+ region: this.options.s3Region,
196
217
  });
197
218
  yield Promise.all(response.map((record) => __awaiter(this, void 0, void 0, function* () {
198
219
  if (record[this.options.pathColumnName]) {
@@ -204,31 +225,39 @@ export default class UploadPlugin extends AdminForthPlugin {
204
225
  }
205
226
  // ** HOOKS FOR DELETE **//
206
227
  // add delete hook which sets tag adminforth-candidate-for-cleanup to true
207
- resourceConfig.hooks.delete.afterSave.push((_h) => __awaiter(this, [_h], void 0, function* ({ record }) {
228
+ resourceConfig.hooks.delete.afterSave.push((_k) => __awaiter(this, [_k], void 0, function* ({ record }) {
208
229
  if (record[pathColumnName]) {
209
- const s3 = new AWS.S3({
210
- accessKeyId: this.options.s3AccessKeyId,
211
- secretAccessKey: this.options.s3SecretAccessKey,
212
- region: this.options.s3Region
230
+ const s3 = new S3({
231
+ credentials: {
232
+ accessKeyId: this.options.s3AccessKeyId,
233
+ secretAccessKey: this.options.s3SecretAccessKey,
234
+ },
235
+ region: this.options.s3Region,
213
236
  });
214
- yield s3.putObjectTagging({
215
- Bucket: this.options.s3Bucket,
216
- Key: record[pathColumnName],
217
- Tagging: {
218
- TagSet: [
219
- {
220
- Key: ADMINFORTH_NOT_YET_USED_TAG,
221
- Value: 'true'
222
- }
223
- ]
224
- }
225
- }).promise();
237
+ try {
238
+ yield s3.putObjectTagging({
239
+ Bucket: this.options.s3Bucket,
240
+ Key: record[pathColumnName],
241
+ Tagging: {
242
+ TagSet: [
243
+ {
244
+ Key: ADMINFORTH_NOT_YET_USED_TAG,
245
+ Value: 'true'
246
+ }
247
+ ]
248
+ }
249
+ });
250
+ }
251
+ catch (e) {
252
+ // file might be e.g. already deleted, so we catch error
253
+ console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${record[pathColumnName]}. File will not be auto-cleaned up`, e);
254
+ }
226
255
  }
227
256
  return { ok: true };
228
257
  }));
229
258
  // ** HOOKS FOR EDIT **//
230
259
  // beforeSave
231
- resourceConfig.hooks.edit.beforeSave.push((_j) => __awaiter(this, [_j], void 0, function* ({ record }) {
260
+ resourceConfig.hooks.edit.beforeSave.push((_l) => __awaiter(this, [_l], void 0, function* ({ record }) {
232
261
  // null is when value is removed
233
262
  if (record[virtualColumn.name] || record[virtualColumn.name] === null) {
234
263
  record[pathColumnName] = record[virtualColumn.name];
@@ -236,37 +265,46 @@ export default class UploadPlugin extends AdminForthPlugin {
236
265
  return { ok: true };
237
266
  }));
238
267
  // add edit postSave hook to delete old file and remove tag from new file
239
- resourceConfig.hooks.edit.afterSave.push((_k) => __awaiter(this, [_k], void 0, function* ({ record, oldRecord }) {
268
+ resourceConfig.hooks.edit.afterSave.push((_m) => __awaiter(this, [_m], void 0, function* ({ record, oldRecord }) {
240
269
  if (record[virtualColumn.name] || record[virtualColumn.name] === null) {
241
- const s3 = new AWS.S3({
242
- accessKeyId: this.options.s3AccessKeyId,
243
- secretAccessKey: this.options.s3SecretAccessKey,
244
- region: this.options.s3Region
270
+ const s3 = new S3({
271
+ credentials: {
272
+ accessKeyId: this.options.s3AccessKeyId,
273
+ secretAccessKey: this.options.s3SecretAccessKey,
274
+ },
275
+ region: this.options.s3Region,
245
276
  });
246
277
  if (oldRecord[pathColumnName]) {
247
278
  // put tag to delete old file
248
- yield s3.putObjectTagging({
249
- Bucket: this.options.s3Bucket,
250
- Key: oldRecord[pathColumnName],
251
- Tagging: {
252
- TagSet: [
253
- {
254
- Key: ADMINFORTH_NOT_YET_USED_TAG,
255
- Value: 'true'
256
- }
257
- ]
258
- }
259
- }).promise();
279
+ try {
280
+ yield s3.putObjectTagging({
281
+ Bucket: this.options.s3Bucket,
282
+ Key: oldRecord[pathColumnName],
283
+ Tagging: {
284
+ TagSet: [
285
+ {
286
+ Key: ADMINFORTH_NOT_YET_USED_TAG,
287
+ Value: 'true'
288
+ }
289
+ ]
290
+ }
291
+ });
292
+ }
293
+ catch (e) {
294
+ // file might be e.g. already deleted, so we catch error
295
+ console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${oldRecord[pathColumnName]}. File will not be auto-cleaned up`, e);
296
+ }
260
297
  }
261
298
  if (record[virtualColumn.name] !== null) {
262
299
  // remove tag from new file
300
+ // in this case we let it crash if it fails: this is a new file which just was uploaded.
263
301
  yield s3.putObjectTagging({
264
302
  Bucket: this.options.s3Bucket,
265
303
  Key: record[pathColumnName],
266
304
  Tagging: {
267
305
  TagSet: []
268
306
  }
269
- }).promise();
307
+ });
270
308
  }
271
309
  }
272
310
  return { ok: true };
@@ -282,30 +320,55 @@ export default class UploadPlugin extends AdminForthPlugin {
282
320
  method: 'POST',
283
321
  path: `/plugin/${this.pluginInstanceId}/get_s3_upload_url`,
284
322
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
323
+ var _b;
285
324
  const { originalFilename, contentType, size, originalExtension } = body;
286
325
  if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
287
- throw new Error(`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`);
326
+ return {
327
+ error: `File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`
328
+ };
288
329
  }
289
330
  const s3Path = this.options.s3Path({ originalFilename, originalExtension, contentType });
290
331
  if (s3Path.startsWith('/')) {
291
332
  throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
292
333
  }
293
- const s3 = new AWS.S3({
294
- accessKeyId: this.options.s3AccessKeyId,
295
- secretAccessKey: this.options.s3SecretAccessKey,
296
- region: this.options.s3Region
334
+ const s3 = new S3({
335
+ credentials: {
336
+ accessKeyId: this.options.s3AccessKeyId,
337
+ secretAccessKey: this.options.s3SecretAccessKey,
338
+ },
339
+ region: this.options.s3Region,
297
340
  });
341
+ const tagline = `${ADMINFORTH_NOT_YET_USED_TAG}=true`;
298
342
  const params = {
299
343
  Bucket: this.options.s3Bucket,
300
344
  Key: s3Path,
301
345
  ContentType: contentType,
302
- ACL: this.options.s3ACL || 'private',
303
- Tagging: `${ADMINFORTH_NOT_YET_USED_TAG}=true`,
346
+ ACL: (this.options.s3ACL || 'private'),
347
+ Tagging: tagline,
304
348
  };
305
- const uploadUrl = yield s3.getSignedUrl('putObject', params);
349
+ const uploadUrl = yield yield getSignedUrl(s3, new PutObjectCommand(params), {
350
+ expiresIn: 1800,
351
+ unhoistableHeaders: new Set(['x-amz-tagging']),
352
+ });
353
+ let previewUrl;
354
+ if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
355
+ previewUrl = this.options.preview.previewUrl({ s3Path });
356
+ return;
357
+ }
358
+ else if (this.options.s3ACL === 'public-read') {
359
+ previewUrl = `https://${this.options.s3Bucket}.s3.${this.options.s3Region}.amazonaws.com/${s3Path}`;
360
+ }
361
+ else {
362
+ previewUrl = yield getSignedUrl(s3, new GetObjectCommand({
363
+ Bucket: this.options.s3Bucket,
364
+ Key: s3Path,
365
+ }));
366
+ }
306
367
  return {
307
368
  uploadUrl,
308
369
  s3Path,
370
+ tagline,
371
+ previewUrl,
309
372
  };
310
373
  })
311
374
  });
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
2
  import { PluginOptions } from './types.js';
3
- import AWS from 'aws-sdk';
3
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4
+ import { ExpirationStatus, GetObjectCommand, ObjectCannedACL, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
4
5
  import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResourcePages, IAdminForth, IHttpServer } from "adminforth";
5
6
 
6
7
  const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup';
@@ -20,13 +21,16 @@ export default class UploadPlugin extends AdminForthPlugin {
20
21
  async setupLifecycleRule() {
21
22
  // check that lifecyle rule "adminforth-unused-cleaner" exists
22
23
  const CLEANUP_RULE_ID = 'adminforth-unused-cleaner';
23
- const s3 = new AWS.S3({
24
- accessKeyId: this.options.s3AccessKeyId,
25
- secretAccessKey: this.options.s3SecretAccessKey,
26
- region: this.options.s3Region
24
+ const s3 = new S3({
25
+ credentials: {
26
+ accessKeyId: this.options.s3AccessKeyId,
27
+ secretAccessKey: this.options.s3SecretAccessKey,
28
+ },
29
+
30
+ region: this.options.s3Region,
27
31
  });
28
32
  // check bucket exists
29
- const bucketExists = s3.headBucket({ Bucket: this.options.s3Bucket }).promise()
33
+ const bucketExists = s3.headBucket({ Bucket: this.options.s3Bucket })
30
34
  if (!bucketExists) {
31
35
  throw new Error(`Bucket ${this.options.s3Bucket} does not exist`);
32
36
  }
@@ -35,10 +39,10 @@ export default class UploadPlugin extends AdminForthPlugin {
35
39
  let ruleExists: boolean = false;
36
40
 
37
41
  try {
38
- const lifecycleConfig: any = await s3.getBucketLifecycleConfiguration({ Bucket: this.options.s3Bucket }).promise();
42
+ const lifecycleConfig: any = await s3.getBucketLifecycleConfiguration({ Bucket: this.options.s3Bucket });
39
43
  ruleExists = lifecycleConfig.Rules.some((rule: any) => rule.ID === CLEANUP_RULE_ID);
40
44
  } catch (e: any) {
41
- if (e.code !== 'NoSuchLifecycleConfiguration') {
45
+ if (e.name !== 'NoSuchLifecycleConfiguration') {
42
46
  throw e;
43
47
  } else {
44
48
  ruleExists = false;
@@ -54,7 +58,7 @@ export default class UploadPlugin extends AdminForthPlugin {
54
58
  Rules: [
55
59
  {
56
60
  ID: CLEANUP_RULE_ID,
57
- Status: 'Enabled',
61
+ Status: ExpirationStatus.Enabled,
58
62
  Filter: {
59
63
  Tag: {
60
64
  Key: ADMINFORTH_NOT_YET_USED_TAG,
@@ -69,19 +73,19 @@ export default class UploadPlugin extends AdminForthPlugin {
69
73
  }
70
74
  };
71
75
 
72
- await s3.putBucketLifecycleConfiguration(params).promise();
76
+ await s3.putBucketLifecycleConfiguration(params);
73
77
  }
74
78
  }
75
79
 
76
- async genPreviewUrl(record: any, s3: AWS.S3) {
80
+ async genPreviewUrl(record: any, s3: S3) {
77
81
  if (this.options.preview?.previewUrl) {
78
82
  record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ s3Path: record[this.options.pathColumnName] });
79
83
  return;
80
84
  }
81
- const previewUrl = await s3.getSignedUrl('getObject', {
85
+ const previewUrl = await await getSignedUrl(s3, new GetObjectCommand({
82
86
  Bucket: this.options.s3Bucket,
83
87
  Key: record[this.options.pathColumnName],
84
- });
88
+ }));
85
89
 
86
90
  record[`previewUrl_${this.pluginInstanceId}`] = previewUrl;
87
91
  }
@@ -111,22 +115,8 @@ export default class UploadPlugin extends AdminForthPlugin {
111
115
  file: this.componentPath('uploader.vue'),
112
116
  meta: pluginFrontendOptions,
113
117
  },
114
- show: {
115
- file: this.componentPath('preview.vue'),
116
- meta: pluginFrontendOptions,
117
- },
118
- ...(
119
- this.options.preview?.showInList ? {
120
- list: {
121
- file: this.componentPath('preview.vue'),
122
- meta: pluginFrontendOptions,
123
- }
124
- } : {}
125
- ),
126
118
  },
127
- showIn: ['edit', 'create', 'show', ...(this.options.preview?.showInList ? [
128
- AdminForthResourcePages.list
129
- ] : [])],
119
+ showIn: ['edit', 'create'],
130
120
  };
131
121
 
132
122
 
@@ -135,6 +125,24 @@ export default class UploadPlugin extends AdminForthPlugin {
135
125
  if (pathColumnIndex === -1) {
136
126
  throw new Error(`Column with name "${pathColumnName}" not found in resource "${resourceConfig.name}"`);
137
127
  }
128
+ if (!resourceConfig.columns[pathColumnIndex].components) {
129
+ resourceConfig.columns[pathColumnIndex].components = {};
130
+ }
131
+
132
+ if (this.options.preview?.showInList || this.options.preview?.showInList === undefined) {
133
+ // add preview column to list
134
+ resourceConfig.columns[pathColumnIndex].components.list = {
135
+ file: this.componentPath('preview.vue'),
136
+ meta: pluginFrontendOptions,
137
+ };
138
+ }
139
+
140
+ if (this.options.preview?.showInShow || this.options.preview?.showInShow === undefined) {
141
+ resourceConfig.columns[pathColumnIndex].components.show = {
142
+ file: this.componentPath('preview.vue'),
143
+ meta: pluginFrontendOptions,
144
+ };
145
+ }
138
146
 
139
147
  // insert virtual column after path column if it is not already there
140
148
  const virtualColumnIndex = resourceConfig.columns.findIndex((column: any) => column.name === virtualColumn.name);
@@ -165,20 +173,26 @@ export default class UploadPlugin extends AdminForthPlugin {
165
173
 
166
174
  // in afterSave hook, aremove tag adminforth-not-yet-used from the file
167
175
  resourceConfig.hooks.create.afterSave.push(async ({ record }: { record: any }) => {
176
+ process.env.HEAVY_DEBUG && console.log('💾💾 after save ', record?.id);
177
+
168
178
  if (record[pathColumnName]) {
169
- const s3 = new AWS.S3({
170
- accessKeyId: this.options.s3AccessKeyId,
171
- secretAccessKey: this.options.s3SecretAccessKey,
172
- region: this.options.s3Region
173
- });
179
+ const s3 = new S3({
180
+ credentials: {
181
+ accessKeyId: this.options.s3AccessKeyId,
182
+ secretAccessKey: this.options.s3SecretAccessKey,
183
+ },
174
184
 
185
+ region: this.options.s3Region,
186
+ });
187
+ process.env.HEAVY_DEBUG && console.log('🪥🪥 remove ObjectTagging', record[pathColumnName]);
188
+ // let it crash if it fails: this is a new file which just was uploaded.
175
189
  await s3.putObjectTagging({
176
190
  Bucket: this.options.s3Bucket,
177
191
  Key: record[pathColumnName],
178
192
  Tagging: {
179
193
  TagSet: []
180
194
  }
181
- }).promise();
195
+ });
182
196
  }
183
197
  return { ok: true };
184
198
  });
@@ -193,10 +207,13 @@ export default class UploadPlugin extends AdminForthPlugin {
193
207
  return { ok: true };
194
208
  }
195
209
  if (record[pathColumnName]) {
196
- const s3 = new AWS.S3({
197
- accessKeyId: this.options.s3AccessKeyId,
198
- secretAccessKey: this.options.s3SecretAccessKey,
199
- region: this.options.s3Region
210
+ const s3 = new S3({
211
+ credentials: {
212
+ accessKeyId: this.options.s3AccessKeyId,
213
+ secretAccessKey: this.options.s3SecretAccessKey,
214
+ },
215
+
216
+ region: this.options.s3Region,
200
217
  });
201
218
 
202
219
  await this.genPreviewUrl(record, s3);
@@ -209,10 +226,13 @@ export default class UploadPlugin extends AdminForthPlugin {
209
226
 
210
227
  if (this.options.preview?.showInList) {
211
228
  resourceConfig.hooks.list.afterDatasourceResponse.push(async ({ response }: { response: any }) => {
212
- const s3 = new AWS.S3({
213
- accessKeyId: this.options.s3AccessKeyId,
214
- secretAccessKey: this.options.s3SecretAccessKey,
215
- region: this.options.s3Region
229
+ const s3 = new S3({
230
+ credentials: {
231
+ accessKeyId: this.options.s3AccessKeyId,
232
+ secretAccessKey: this.options.s3SecretAccessKey,
233
+ },
234
+
235
+ region: this.options.s3Region,
216
236
  });
217
237
 
218
238
  await Promise.all(response.map(async (record: any) => {
@@ -229,24 +249,32 @@ export default class UploadPlugin extends AdminForthPlugin {
229
249
  // add delete hook which sets tag adminforth-candidate-for-cleanup to true
230
250
  resourceConfig.hooks.delete.afterSave.push(async ({ record }: { record: any }) => {
231
251
  if (record[pathColumnName]) {
232
- const s3 = new AWS.S3({
233
- accessKeyId: this.options.s3AccessKeyId,
234
- secretAccessKey: this.options.s3SecretAccessKey,
235
- region: this.options.s3Region
252
+ const s3 = new S3({
253
+ credentials: {
254
+ accessKeyId: this.options.s3AccessKeyId,
255
+ secretAccessKey: this.options.s3SecretAccessKey,
256
+ },
257
+
258
+ region: this.options.s3Region,
236
259
  });
237
260
 
238
- await s3.putObjectTagging({
239
- Bucket: this.options.s3Bucket,
240
- Key: record[pathColumnName],
241
- Tagging: {
242
- TagSet: [
243
- {
244
- Key: ADMINFORTH_NOT_YET_USED_TAG,
245
- Value: 'true'
246
- }
247
- ]
248
- }
249
- }).promise();
261
+ try {
262
+ await s3.putObjectTagging({
263
+ Bucket: this.options.s3Bucket,
264
+ Key: record[pathColumnName],
265
+ Tagging: {
266
+ TagSet: [
267
+ {
268
+ Key: ADMINFORTH_NOT_YET_USED_TAG,
269
+ Value: 'true'
270
+ }
271
+ ]
272
+ }
273
+ });
274
+ } catch (e) {
275
+ // file might be e.g. already deleted, so we catch error
276
+ console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${record[pathColumnName]}. File will not be auto-cleaned up`, e);
277
+ }
250
278
  }
251
279
  return { ok: true };
252
280
  });
@@ -268,36 +296,45 @@ export default class UploadPlugin extends AdminForthPlugin {
268
296
  resourceConfig.hooks.edit.afterSave.push(async ({ record, oldRecord }: { record: any, oldRecord: any }) => {
269
297
 
270
298
  if (record[virtualColumn.name] || record[virtualColumn.name] === null) {
271
- const s3 = new AWS.S3({
272
- accessKeyId: this.options.s3AccessKeyId,
273
- secretAccessKey: this.options.s3SecretAccessKey,
274
- region: this.options.s3Region
299
+ const s3 = new S3({
300
+ credentials: {
301
+ accessKeyId: this.options.s3AccessKeyId,
302
+ secretAccessKey: this.options.s3SecretAccessKey,
303
+ },
304
+
305
+ region: this.options.s3Region,
275
306
  });
276
307
 
277
308
  if (oldRecord[pathColumnName]) {
278
309
  // put tag to delete old file
279
- await s3.putObjectTagging({
280
- Bucket: this.options.s3Bucket,
281
- Key: oldRecord[pathColumnName],
282
- Tagging: {
283
- TagSet: [
284
- {
285
- Key: ADMINFORTH_NOT_YET_USED_TAG,
286
- Value: 'true'
287
- }
288
- ]
289
- }
290
- }).promise();
310
+ try {
311
+ await s3.putObjectTagging({
312
+ Bucket: this.options.s3Bucket,
313
+ Key: oldRecord[pathColumnName],
314
+ Tagging: {
315
+ TagSet: [
316
+ {
317
+ Key: ADMINFORTH_NOT_YET_USED_TAG,
318
+ Value: 'true'
319
+ }
320
+ ]
321
+ }
322
+ });
323
+ } catch (e) {
324
+ // file might be e.g. already deleted, so we catch error
325
+ console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${oldRecord[pathColumnName]}. File will not be auto-cleaned up`, e);
326
+ }
291
327
  }
292
328
  if (record[virtualColumn.name] !== null) {
293
329
  // remove tag from new file
330
+ // in this case we let it crash if it fails: this is a new file which just was uploaded.
294
331
  await s3.putObjectTagging({
295
332
  Bucket: this.options.s3Bucket,
296
333
  Key: record[pathColumnName],
297
334
  Tagging: {
298
335
  TagSet: []
299
336
  }
300
- }).promise();
337
+ });
301
338
  }
302
339
  }
303
340
  return { ok: true };
@@ -319,33 +356,56 @@ export default class UploadPlugin extends AdminForthPlugin {
319
356
  const { originalFilename, contentType, size, originalExtension } = body;
320
357
 
321
358
  if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
322
- throw new Error(`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`);
359
+ return {
360
+ error: `File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`
361
+ };
323
362
  }
324
363
 
325
364
  const s3Path: string = this.options.s3Path({ originalFilename, originalExtension, contentType });
326
365
  if (s3Path.startsWith('/')) {
327
366
  throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
328
367
  }
329
- const s3 = new AWS.S3({
330
- accessKeyId: this.options.s3AccessKeyId,
331
- secretAccessKey: this.options.s3SecretAccessKey,
332
- region: this.options.s3Region
368
+ const s3 = new S3({
369
+ credentials: {
370
+ accessKeyId: this.options.s3AccessKeyId,
371
+ secretAccessKey: this.options.s3SecretAccessKey,
372
+ },
373
+
374
+ region: this.options.s3Region,
333
375
  });
334
376
 
377
+ const tagline = `${ADMINFORTH_NOT_YET_USED_TAG}=true`;
335
378
  const params = {
336
379
  Bucket: this.options.s3Bucket,
337
380
  Key: s3Path,
338
381
  ContentType: contentType,
339
- ACL: this.options.s3ACL || 'private',
340
- Tagging: `${ADMINFORTH_NOT_YET_USED_TAG}=true`,
382
+ ACL: (this.options.s3ACL || 'private') as ObjectCannedACL,
383
+ Tagging: tagline,
341
384
  };
342
385
 
343
- const uploadUrl = await s3.getSignedUrl('putObject', params,)
386
+ const uploadUrl = await await getSignedUrl(s3, new PutObjectCommand(params), {
387
+ expiresIn: 1800,
388
+ unhoistableHeaders: new Set(['x-amz-tagging']),
389
+ });
344
390
 
391
+ let previewUrl;
392
+ if (this.options.preview?.previewUrl) {
393
+ previewUrl = this.options.preview.previewUrl({ s3Path });
394
+ return;
395
+ } else if (this.options.s3ACL === 'public-read') {
396
+ previewUrl = `https://${this.options.s3Bucket}.s3.${this.options.s3Region}.amazonaws.com/${s3Path}`;
397
+ } else {
398
+ previewUrl = await getSignedUrl(s3, new GetObjectCommand({
399
+ Bucket: this.options.s3Bucket,
400
+ Key: s3Path,
401
+ }));
402
+ }
345
403
 
346
404
  return {
347
405
  uploadUrl,
348
406
  s3Path,
407
+ tagline,
408
+ previewUrl,
349
409
  };
350
410
  }
351
411
  });
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
- "rollout": "tsc && cp -rf custom dist/ && npm version patch && npm publish --access public",
8
+ "rollout": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch && npm publish --access public",
9
9
  "prepare": "npm link adminforth",
10
10
  "build": "tsc"
11
11
  },
@@ -13,6 +13,7 @@
13
13
  "author": "",
14
14
  "license": "ISC",
15
15
  "dependencies": {
16
- "aws-sdk": "^2.1654.0"
16
+ "@aws-sdk/client-s3": "^3.629.0",
17
+ "@aws-sdk/s3-request-presigner": "^3.629.0"
17
18
  }
18
19
  }
package/types.ts CHANGED
@@ -60,9 +60,16 @@ export type PluginOptions = {
60
60
  preview: {
61
61
 
62
62
  /**
63
- * By default preview is shown in the show view only. If you want to show it in the list view as well, set this to true
63
+ * Whether to show preview of image instead of path in list field
64
+ * By default true
64
65
  */
65
- showInList: boolean,
66
+ showInList?: boolean,
67
+
68
+ /**
69
+ * Whether to show preview of image instead of path in list field
70
+ * By default true
71
+ */
72
+ showInShow?: boolean,
66
73
 
67
74
  /**
68
75
  * Used to display preview (if it is image) in list and show views.