@adminforth/upload 2.11.0 → 2.12.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 +4 -1
- package/dist/custom/preview.vue +4 -1
- package/dist/index.js +130 -2
- package/index.ts +162 -7
- package/package.json +1 -1
- package/types.ts +13 -0
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
|
|
15
|
-
total size is 53,
|
|
14
|
+
sent 54,028 bytes received 134 bytes 108,324.00 bytes/sec
|
|
15
|
+
total size is 53,539 speedup is 0.99
|
package/custom/preview.vue
CHANGED
|
@@ -84,7 +84,10 @@ onMounted(async () => {
|
|
|
84
84
|
if (Array.isArray(url.value)) return;
|
|
85
85
|
try {
|
|
86
86
|
const response = await fetch(url.value, {
|
|
87
|
-
method: '
|
|
87
|
+
method: 'GET',
|
|
88
|
+
headers: {
|
|
89
|
+
Range: 'bytes=0-0' // this approach is better then HEAD in terms of CDN/CORS, but same for efficiency as HEAD
|
|
90
|
+
},
|
|
88
91
|
mode: 'cors',
|
|
89
92
|
});
|
|
90
93
|
const ct = response.headers.get('Content-Type');
|
package/dist/custom/preview.vue
CHANGED
|
@@ -84,7 +84,10 @@ onMounted(async () => {
|
|
|
84
84
|
if (Array.isArray(url.value)) return;
|
|
85
85
|
try {
|
|
86
86
|
const response = await fetch(url.value, {
|
|
87
|
-
method: '
|
|
87
|
+
method: 'GET',
|
|
88
|
+
headers: {
|
|
89
|
+
Range: 'bytes=0-0' // this approach is better then HEAD in terms of CDN/CORS, but same for efficiency as HEAD
|
|
90
|
+
},
|
|
88
91
|
mode: 'cors',
|
|
89
92
|
});
|
|
90
93
|
const ct = response.headers.get('Content-Type');
|
package/dist/index.js
CHANGED
|
@@ -485,7 +485,7 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
485
485
|
/*
|
|
486
486
|
* Uploads a file from a buffer, creates a record in the resource, and returns the file path and preview URL.
|
|
487
487
|
*/
|
|
488
|
-
|
|
488
|
+
uploadFromBufferToNewRecord(_a) {
|
|
489
489
|
return __awaiter(this, arguments, void 0, function* ({ filename, contentType, buffer, adminUser, extra, recordAttributes, }) {
|
|
490
490
|
var _b;
|
|
491
491
|
if (!filename || !contentType || !buffer) {
|
|
@@ -554,7 +554,7 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
554
554
|
if (!this.resourceConfig) {
|
|
555
555
|
throw new Error('resourceConfig is not initialized yet');
|
|
556
556
|
}
|
|
557
|
-
const { error: createError } = yield this.adminforth.createResourceRecord({
|
|
557
|
+
const { error: createError, createdRecord, newRecordId } = yield this.adminforth.createResourceRecord({
|
|
558
558
|
resource: this.resourceConfig,
|
|
559
559
|
record: Object.assign(Object.assign({}, (recordAttributes !== null && recordAttributes !== void 0 ? recordAttributes : {})), { [this.options.pathColumnName]: filePath }),
|
|
560
560
|
adminUser,
|
|
@@ -569,6 +569,134 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
569
569
|
}
|
|
570
570
|
throw new Error(`Error creating record after upload: ${createError}`);
|
|
571
571
|
}
|
|
572
|
+
const pkColumn = this.resourceConfig.columns.find((column) => column.primaryKey);
|
|
573
|
+
const pkName = pkColumn === null || pkColumn === void 0 ? void 0 : pkColumn.name;
|
|
574
|
+
const newRecordPk = newRecordId !== null && newRecordId !== void 0 ? newRecordId : (pkName && createdRecord ? createdRecord[pkName] : undefined);
|
|
575
|
+
let previewUrl;
|
|
576
|
+
if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
|
|
577
|
+
previewUrl = this.options.preview.previewUrl({ filePath });
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
previewUrl = yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
path: filePath,
|
|
584
|
+
previewUrl,
|
|
585
|
+
newRecordPk,
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/*
|
|
590
|
+
* Uploads a file from a buffer and updates an existing record's path column.
|
|
591
|
+
* If the newly generated storage path would be the same as the current path,
|
|
592
|
+
* throws an error to avoid potential caching issues.
|
|
593
|
+
*/
|
|
594
|
+
uploadFromBufferToExistingRecord(_a) {
|
|
595
|
+
return __awaiter(this, arguments, void 0, function* ({ recordId, filename, contentType, buffer, adminUser, extra, }) {
|
|
596
|
+
var _b;
|
|
597
|
+
if (recordId === undefined || recordId === null) {
|
|
598
|
+
throw new Error('recordId is required');
|
|
599
|
+
}
|
|
600
|
+
if (!filename || !contentType || !buffer) {
|
|
601
|
+
throw new Error('filename, contentType and buffer are required');
|
|
602
|
+
}
|
|
603
|
+
if (!this.resourceConfig) {
|
|
604
|
+
throw new Error('resourceConfig is not initialized yet');
|
|
605
|
+
}
|
|
606
|
+
const pkColumn = this.resourceConfig.columns.find((column) => column.primaryKey);
|
|
607
|
+
const pkName = pkColumn === null || pkColumn === void 0 ? void 0 : pkColumn.name;
|
|
608
|
+
if (!pkName) {
|
|
609
|
+
throw new Error('Primary key column not found in resource configuration');
|
|
610
|
+
}
|
|
611
|
+
const existingRecord = yield this.adminforth
|
|
612
|
+
.resource(this.resourceConfig.resourceId)
|
|
613
|
+
.get([Filters.EQ(pkName, recordId)]);
|
|
614
|
+
if (!existingRecord) {
|
|
615
|
+
throw new Error(`Record with id ${recordId} not found`);
|
|
616
|
+
}
|
|
617
|
+
const lastDotIndex = filename.lastIndexOf('.');
|
|
618
|
+
if (lastDotIndex === -1) {
|
|
619
|
+
throw new Error('filename must contain an extension');
|
|
620
|
+
}
|
|
621
|
+
const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
|
|
622
|
+
const originalFilename = filename.substring(0, lastDotIndex);
|
|
623
|
+
if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
|
|
624
|
+
throw new Error(`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`);
|
|
625
|
+
}
|
|
626
|
+
let nodeBuffer;
|
|
627
|
+
if (Buffer.isBuffer(buffer)) {
|
|
628
|
+
nodeBuffer = buffer;
|
|
629
|
+
}
|
|
630
|
+
else if (buffer instanceof ArrayBuffer) {
|
|
631
|
+
nodeBuffer = Buffer.from(buffer);
|
|
632
|
+
}
|
|
633
|
+
else if (ArrayBuffer.isView(buffer)) {
|
|
634
|
+
nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
throw new Error('Unsupported buffer type');
|
|
638
|
+
}
|
|
639
|
+
const size = nodeBuffer.byteLength;
|
|
640
|
+
if (this.options.maxFileSize && size > this.options.maxFileSize) {
|
|
641
|
+
throw new Error(`File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`);
|
|
642
|
+
}
|
|
643
|
+
const existingValue = existingRecord[this.options.pathColumnName];
|
|
644
|
+
const existingPaths = this.normalizePaths(existingValue);
|
|
645
|
+
const filePath = this.options.filePath({
|
|
646
|
+
originalFilename,
|
|
647
|
+
originalExtension,
|
|
648
|
+
contentType,
|
|
649
|
+
record: existingRecord,
|
|
650
|
+
});
|
|
651
|
+
if (filePath.startsWith('/')) {
|
|
652
|
+
throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
|
|
653
|
+
}
|
|
654
|
+
if (existingPaths.includes(filePath)) {
|
|
655
|
+
throw new Error('New file path cannot be the same as existing path to avoid caching issues');
|
|
656
|
+
}
|
|
657
|
+
const { uploadUrl, uploadExtraParams } = yield this.options.storageAdapter.getUploadSignedUrl(filePath, contentType, 1800);
|
|
658
|
+
const headers = {
|
|
659
|
+
'Content-Type': contentType,
|
|
660
|
+
};
|
|
661
|
+
if (uploadExtraParams) {
|
|
662
|
+
Object.entries(uploadExtraParams).forEach(([key, value]) => {
|
|
663
|
+
headers[key] = value;
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
const resp = yield fetch(uploadUrl, {
|
|
667
|
+
method: 'PUT',
|
|
668
|
+
headers,
|
|
669
|
+
body: nodeBuffer,
|
|
670
|
+
});
|
|
671
|
+
if (!resp.ok) {
|
|
672
|
+
let bodyText = '';
|
|
673
|
+
try {
|
|
674
|
+
bodyText = yield resp.text();
|
|
675
|
+
}
|
|
676
|
+
catch (e) {
|
|
677
|
+
// ignore
|
|
678
|
+
}
|
|
679
|
+
throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
|
|
680
|
+
}
|
|
681
|
+
const { error: updateError } = yield this.adminforth.updateResourceRecord({
|
|
682
|
+
resource: this.resourceConfig,
|
|
683
|
+
recordId,
|
|
684
|
+
oldRecord: existingRecord,
|
|
685
|
+
adminUser,
|
|
686
|
+
extra,
|
|
687
|
+
updates: {
|
|
688
|
+
[this.options.pathColumnName]: filePath,
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
if (updateError) {
|
|
692
|
+
try {
|
|
693
|
+
yield this.markKeyForDeletion(filePath);
|
|
694
|
+
}
|
|
695
|
+
catch (e) {
|
|
696
|
+
// best-effort cleanup, ignore error
|
|
697
|
+
}
|
|
698
|
+
throw new Error(`Error updating record after upload: ${updateError}`);
|
|
699
|
+
}
|
|
572
700
|
let previewUrl;
|
|
573
701
|
if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
|
|
574
702
|
previewUrl = this.options.preview.previewUrl({ filePath });
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { PluginOptions, UploadFromBufferParams } from './types.js';
|
|
2
|
+
import { PluginOptions, UploadFromBufferParams, UploadFromBufferToExistingRecordParams } from './types.js';
|
|
3
3
|
import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo, RateLimiter, AdminUser, HttpExtra } from "adminforth";
|
|
4
4
|
import { Readable } from "stream";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
@@ -549,22 +549,19 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
549
549
|
return { error: 'failed to generate preview URL' };
|
|
550
550
|
},
|
|
551
551
|
});
|
|
552
|
-
|
|
553
|
-
|
|
554
552
|
}
|
|
555
553
|
|
|
556
|
-
|
|
557
554
|
/*
|
|
558
555
|
* Uploads a file from a buffer, creates a record in the resource, and returns the file path and preview URL.
|
|
559
556
|
*/
|
|
560
|
-
async
|
|
557
|
+
async uploadFromBufferToNewRecord({
|
|
561
558
|
filename,
|
|
562
559
|
contentType,
|
|
563
560
|
buffer,
|
|
564
561
|
adminUser,
|
|
565
562
|
extra,
|
|
566
563
|
recordAttributes,
|
|
567
|
-
}: UploadFromBufferParams): Promise<{ path: string; previewUrl: string }> {
|
|
564
|
+
}: UploadFromBufferParams): Promise<{ path: string; previewUrl: string; newRecordPk: any }> {
|
|
568
565
|
if (!filename || !contentType || !buffer) {
|
|
569
566
|
throw new Error('filename, contentType and buffer are required');
|
|
570
567
|
}
|
|
@@ -648,7 +645,7 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
648
645
|
throw new Error('resourceConfig is not initialized yet');
|
|
649
646
|
}
|
|
650
647
|
|
|
651
|
-
const { error: createError } = await this.adminforth.createResourceRecord({
|
|
648
|
+
const { error: createError, createdRecord, newRecordId }: any = await this.adminforth.createResourceRecord({
|
|
652
649
|
resource: this.resourceConfig,
|
|
653
650
|
record: { ...(recordAttributes ?? {}), [this.options.pathColumnName]: filePath },
|
|
654
651
|
adminUser,
|
|
@@ -664,6 +661,164 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
664
661
|
throw new Error(`Error creating record after upload: ${createError}`);
|
|
665
662
|
}
|
|
666
663
|
|
|
664
|
+
const pkColumn = this.resourceConfig.columns.find((column: any) => column.primaryKey);
|
|
665
|
+
const pkName = pkColumn?.name;
|
|
666
|
+
const newRecordPk = newRecordId ?? (pkName && createdRecord ? createdRecord[pkName] : undefined);
|
|
667
|
+
|
|
668
|
+
let previewUrl: string;
|
|
669
|
+
if (this.options.preview?.previewUrl) {
|
|
670
|
+
previewUrl = this.options.preview.previewUrl({ filePath });
|
|
671
|
+
} else {
|
|
672
|
+
previewUrl = await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
path: filePath,
|
|
677
|
+
previewUrl,
|
|
678
|
+
newRecordPk,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/*
|
|
683
|
+
* Uploads a file from a buffer and updates an existing record's path column.
|
|
684
|
+
* If the newly generated storage path would be the same as the current path,
|
|
685
|
+
* throws an error to avoid potential caching issues.
|
|
686
|
+
*/
|
|
687
|
+
async uploadFromBufferToExistingRecord({
|
|
688
|
+
recordId,
|
|
689
|
+
filename,
|
|
690
|
+
contentType,
|
|
691
|
+
buffer,
|
|
692
|
+
adminUser,
|
|
693
|
+
extra,
|
|
694
|
+
}: UploadFromBufferToExistingRecordParams): Promise<{ path: string; previewUrl: string }> {
|
|
695
|
+
if (recordId === undefined || recordId === null) {
|
|
696
|
+
throw new Error('recordId is required');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!filename || !contentType || !buffer) {
|
|
700
|
+
throw new Error('filename, contentType and buffer are required');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!this.resourceConfig) {
|
|
704
|
+
throw new Error('resourceConfig is not initialized yet');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const pkColumn = this.resourceConfig.columns.find((column: any) => column.primaryKey);
|
|
708
|
+
const pkName = pkColumn?.name;
|
|
709
|
+
if (!pkName) {
|
|
710
|
+
throw new Error('Primary key column not found in resource configuration');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const existingRecord = await this.adminforth
|
|
714
|
+
.resource(this.resourceConfig.resourceId)
|
|
715
|
+
.get([Filters.EQ(pkName, recordId)]);
|
|
716
|
+
|
|
717
|
+
if (!existingRecord) {
|
|
718
|
+
throw new Error(`Record with id ${recordId} not found`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const lastDotIndex = filename.lastIndexOf('.');
|
|
722
|
+
if (lastDotIndex === -1) {
|
|
723
|
+
throw new Error('filename must contain an extension');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
|
|
727
|
+
const originalFilename = filename.substring(0, lastDotIndex);
|
|
728
|
+
|
|
729
|
+
if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
let nodeBuffer: Buffer;
|
|
736
|
+
if (Buffer.isBuffer(buffer)) {
|
|
737
|
+
nodeBuffer = buffer;
|
|
738
|
+
} else if (buffer instanceof ArrayBuffer) {
|
|
739
|
+
nodeBuffer = Buffer.from(buffer);
|
|
740
|
+
} else if (ArrayBuffer.isView(buffer)) {
|
|
741
|
+
nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
742
|
+
} else {
|
|
743
|
+
throw new Error('Unsupported buffer type');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const size = nodeBuffer.byteLength;
|
|
747
|
+
if (this.options.maxFileSize && size > this.options.maxFileSize) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const existingValue = existingRecord[this.options.pathColumnName];
|
|
754
|
+
const existingPaths = this.normalizePaths(existingValue);
|
|
755
|
+
|
|
756
|
+
const filePath: string = this.options.filePath({
|
|
757
|
+
originalFilename,
|
|
758
|
+
originalExtension,
|
|
759
|
+
contentType,
|
|
760
|
+
record: existingRecord,
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
if (filePath.startsWith('/')) {
|
|
764
|
+
throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (existingPaths.includes(filePath)) {
|
|
768
|
+
throw new Error('New file path cannot be the same as existing path to avoid caching issues');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const { uploadUrl, uploadExtraParams } = await this.options.storageAdapter.getUploadSignedUrl(
|
|
772
|
+
filePath,
|
|
773
|
+
contentType,
|
|
774
|
+
1800,
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
const headers: Record<string, string> = {
|
|
778
|
+
'Content-Type': contentType,
|
|
779
|
+
};
|
|
780
|
+
if (uploadExtraParams) {
|
|
781
|
+
Object.entries(uploadExtraParams).forEach(([key, value]) => {
|
|
782
|
+
headers[key] = value as string;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const resp = await fetch(uploadUrl as any, {
|
|
787
|
+
method: 'PUT',
|
|
788
|
+
headers,
|
|
789
|
+
body: nodeBuffer as any,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
if (!resp.ok) {
|
|
793
|
+
let bodyText = '';
|
|
794
|
+
try {
|
|
795
|
+
bodyText = await resp.text();
|
|
796
|
+
} catch (e) {
|
|
797
|
+
// ignore
|
|
798
|
+
}
|
|
799
|
+
throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const { error: updateError } = await this.adminforth.updateResourceRecord({
|
|
803
|
+
resource: this.resourceConfig,
|
|
804
|
+
recordId,
|
|
805
|
+
oldRecord: existingRecord,
|
|
806
|
+
adminUser,
|
|
807
|
+
extra,
|
|
808
|
+
updates: {
|
|
809
|
+
[this.options.pathColumnName]: filePath,
|
|
810
|
+
},
|
|
811
|
+
} as any);
|
|
812
|
+
|
|
813
|
+
if (updateError) {
|
|
814
|
+
try {
|
|
815
|
+
await this.markKeyForDeletion(filePath);
|
|
816
|
+
} catch (e) {
|
|
817
|
+
// best-effort cleanup, ignore error
|
|
818
|
+
}
|
|
819
|
+
throw new Error(`Error updating record after upload: ${updateError}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
667
822
|
let previewUrl: string;
|
|
668
823
|
if (this.options.preview?.previewUrl) {
|
|
669
824
|
previewUrl = this.options.preview.previewUrl({ filePath });
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -199,4 +199,17 @@ export type UploadFromBufferParams = {
|
|
|
199
199
|
* Values here do NOT affect the generated storage path.
|
|
200
200
|
*/
|
|
201
201
|
recordAttributes?: Record<string, any>;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parameters for the UploadPlugin.uploadFromBufferToExistingRecord API.
|
|
206
|
+
* Used to upload a binary file buffer and update the path column
|
|
207
|
+
* of an already existing record identified by its primary key.
|
|
208
|
+
*/
|
|
209
|
+
export type UploadFromBufferToExistingRecordParams = UploadFromBufferParams & {
|
|
210
|
+
/**
|
|
211
|
+
* Primary key value of the existing record whose file path
|
|
212
|
+
* should be replaced.
|
|
213
|
+
*/
|
|
214
|
+
recordId: any;
|
|
202
215
|
};
|