@adminforth/upload 1.4.7 → 2.0.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 +2 -2
- package/custom/preview.vue +0 -1
- package/custom/uploader.vue +6 -4
- package/dist/custom/preview.vue +0 -1
- package/dist/custom/uploader.vue +6 -4
- package/dist/index.js +18 -170
- package/index.ts +18 -186
- package/package.json +1 -1
- package/types.ts +8 -29
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,
|
|
15
|
-
total size is 42,
|
|
14
|
+
sent 43,158 bytes received 134 bytes 86,584.00 bytes/sec
|
|
15
|
+
total size is 42,676 speedup is 0.99
|
package/custom/preview.vue
CHANGED
package/custom/uploader.vue
CHANGED
|
@@ -229,8 +229,8 @@ const onFileChange = async (e) => {
|
|
|
229
229
|
reader.readAsDataURL(file);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
const { uploadUrl,
|
|
233
|
-
path: `/plugin/${props.meta.pluginInstanceId}/
|
|
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
|
-
|
|
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',
|
|
289
|
+
emit('update:value', filePath);
|
|
288
290
|
} catch (error) {
|
|
289
291
|
console.error('Error uploading file:', error);
|
|
290
292
|
adminforth.alert({
|
package/dist/custom/preview.vue
CHANGED
package/dist/custom/uploader.vue
CHANGED
|
@@ -229,8 +229,8 @@ const onFileChange = async (e) => {
|
|
|
229
229
|
reader.readAsDataURL(file);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
const { uploadUrl,
|
|
233
|
-
path: `/plugin/${props.meta.pluginInstanceId}/
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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.storageAdapter.setupLifecycle();
|
|
84
28
|
});
|
|
85
29
|
}
|
|
86
|
-
genPreviewUrl(record
|
|
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({
|
|
34
|
+
record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ filePath: record[this.options.pathColumnName] });
|
|
91
35
|
return;
|
|
92
36
|
}
|
|
93
|
-
const previewUrl = yield
|
|
94
|
-
Bucket: this.options.s3Bucket,
|
|
95
|
-
Key: record[this.options.pathColumnName],
|
|
96
|
-
}));
|
|
37
|
+
const previewUrl = yield this.options.storageAdapter.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
|
|
218
|
-
Bucket: this.options.s3Bucket,
|
|
219
|
-
Key: record[pathColumnName],
|
|
220
|
-
Tagging: {
|
|
221
|
-
TagSet: []
|
|
222
|
-
}
|
|
223
|
-
});
|
|
151
|
+
yield this.options.storageAdapter.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
|
-
|
|
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
|
|
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
|
|
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.storageAdapter.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
|
|
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.storageAdapter.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
|
|
342
|
-
Bucket: this.options.s3Bucket,
|
|
343
|
-
Key: updates[pathColumnName],
|
|
344
|
-
Tagging: {
|
|
345
|
-
TagSet: []
|
|
346
|
-
}
|
|
347
|
-
});
|
|
219
|
+
yield this.options.storageAdapter.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}/
|
|
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
|
|
389
|
-
if (
|
|
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
|
|
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.storageAdapter.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({
|
|
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
|
|
420
|
-
Bucket: this.options.s3Bucket,
|
|
421
|
-
Key: s3Path,
|
|
422
|
-
}));
|
|
270
|
+
previewUrl = yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
423
271
|
}
|
|
424
272
|
return {
|
|
425
273
|
uploadUrl,
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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.storageAdapter.setupLifecycle();
|
|
92
32
|
}
|
|
93
33
|
|
|
94
|
-
async genPreviewUrl(record: any
|
|
34
|
+
async genPreviewUrl(record: any) {
|
|
95
35
|
if (this.options.preview?.previewUrl) {
|
|
96
|
-
record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({
|
|
36
|
+
record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ filePath: record[this.options.pathColumnName] });
|
|
97
37
|
return;
|
|
98
38
|
}
|
|
99
|
-
const previewUrl = await
|
|
100
|
-
Bucket: this.options.s3Bucket,
|
|
101
|
-
Key: record[this.options.pathColumnName],
|
|
102
|
-
}));
|
|
39
|
+
const previewUrl = await this.options.storageAdapter.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
|
|
236
|
-
Bucket: this.options.s3Bucket,
|
|
237
|
-
Key: record[pathColumnName],
|
|
238
|
-
Tagging: {
|
|
239
|
-
TagSet: []
|
|
240
|
-
}
|
|
241
|
-
});
|
|
164
|
+
await this.options.storageAdapter.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
|
-
|
|
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
|
|
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
|
|
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.storageAdapter.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
|
|
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.storageAdapter.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
|
|
381
|
-
Bucket: this.options.s3Bucket,
|
|
382
|
-
Key: updates[pathColumnName],
|
|
383
|
-
Tagging: {
|
|
384
|
-
TagSet: []
|
|
385
|
-
}
|
|
386
|
-
});
|
|
245
|
+
await this.options.storageAdapter.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}/
|
|
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
|
|
437
|
-
if (
|
|
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
|
|
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.storageAdapter.getUploadSignedUrl(filePath, contentType, 1800);
|
|
463
300
|
let previewUrl;
|
|
464
301
|
if (this.options.preview?.previewUrl) {
|
|
465
|
-
previewUrl = this.options.preview.previewUrl({
|
|
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
|
|
470
|
-
Bucket: this.options.s3Bucket,
|
|
471
|
-
Key: s3Path,
|
|
472
|
-
}));
|
|
304
|
+
previewUrl = await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
473
305
|
}
|
|
474
306
|
|
|
475
307
|
return {
|
|
476
308
|
uploadUrl,
|
|
477
|
-
|
|
478
|
-
|
|
309
|
+
filePath,
|
|
310
|
+
uploadExtraParams,
|
|
479
311
|
previewUrl,
|
|
480
312
|
};
|
|
481
313
|
}
|
package/package.json
CHANGED
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
|
-
|
|
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?: ({
|
|
90
|
+
previewUrl?: ({filePath}) => string,
|
|
117
91
|
}
|
|
118
92
|
|
|
119
93
|
|
|
@@ -181,4 +155,9 @@ export type PluginOptions = {
|
|
|
181
155
|
|
|
182
156
|
}
|
|
183
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The adapter used to store the files.
|
|
160
|
+
* For now only S3 adapter is supported.
|
|
161
|
+
*/
|
|
162
|
+
storageAdapter: StorageAdapter,
|
|
184
163
|
}
|