@adminforth/upload 2.9.0 → 2.11.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 +1 -1
- package/dist/index.js +101 -2
- package/index.ts +125 -3
- package/package.json +1 -1
- package/types.ts +47 -2
package/build.log
CHANGED
package/dist/index.js
CHANGED
|
@@ -7,9 +7,8 @@ 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 { AdminForthPlugin, Filters, suggestIfTypo } from "adminforth";
|
|
10
|
+
import { AdminForthPlugin, Filters, suggestIfTypo, RateLimiter } from "adminforth";
|
|
11
11
|
import { Readable } from "stream";
|
|
12
|
-
import { RateLimiter } from "adminforth";
|
|
13
12
|
import { randomUUID } from "crypto";
|
|
14
13
|
import { interpretResource } from 'adminforth';
|
|
15
14
|
import { ActionCheckSource } from 'adminforth';
|
|
@@ -483,4 +482,104 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
483
482
|
}),
|
|
484
483
|
});
|
|
485
484
|
}
|
|
485
|
+
/*
|
|
486
|
+
* Uploads a file from a buffer, creates a record in the resource, and returns the file path and preview URL.
|
|
487
|
+
*/
|
|
488
|
+
uploadFromBuffer(_a) {
|
|
489
|
+
return __awaiter(this, arguments, void 0, function* ({ filename, contentType, buffer, adminUser, extra, recordAttributes, }) {
|
|
490
|
+
var _b;
|
|
491
|
+
if (!filename || !contentType || !buffer) {
|
|
492
|
+
throw new Error('filename, contentType and buffer are required');
|
|
493
|
+
}
|
|
494
|
+
const lastDotIndex = filename.lastIndexOf('.');
|
|
495
|
+
if (lastDotIndex === -1) {
|
|
496
|
+
throw new Error('filename must contain an extension');
|
|
497
|
+
}
|
|
498
|
+
const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
|
|
499
|
+
const originalFilename = filename.substring(0, lastDotIndex);
|
|
500
|
+
if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
|
|
501
|
+
throw new Error(`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`);
|
|
502
|
+
}
|
|
503
|
+
let nodeBuffer;
|
|
504
|
+
if (Buffer.isBuffer(buffer)) {
|
|
505
|
+
nodeBuffer = buffer;
|
|
506
|
+
}
|
|
507
|
+
else if (buffer instanceof ArrayBuffer) {
|
|
508
|
+
nodeBuffer = Buffer.from(buffer);
|
|
509
|
+
}
|
|
510
|
+
else if (ArrayBuffer.isView(buffer)) {
|
|
511
|
+
nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
throw new Error('Unsupported buffer type');
|
|
515
|
+
}
|
|
516
|
+
const size = nodeBuffer.byteLength;
|
|
517
|
+
if (this.options.maxFileSize && size > this.options.maxFileSize) {
|
|
518
|
+
throw new Error(`File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`);
|
|
519
|
+
}
|
|
520
|
+
const filePath = this.options.filePath({
|
|
521
|
+
originalFilename,
|
|
522
|
+
originalExtension,
|
|
523
|
+
contentType,
|
|
524
|
+
record: undefined,
|
|
525
|
+
});
|
|
526
|
+
if (filePath.startsWith('/')) {
|
|
527
|
+
throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
|
|
528
|
+
}
|
|
529
|
+
const { uploadUrl, uploadExtraParams } = yield this.options.storageAdapter.getUploadSignedUrl(filePath, contentType, 1800);
|
|
530
|
+
const headers = {
|
|
531
|
+
'Content-Type': contentType,
|
|
532
|
+
};
|
|
533
|
+
if (uploadExtraParams) {
|
|
534
|
+
Object.entries(uploadExtraParams).forEach(([key, value]) => {
|
|
535
|
+
headers[key] = value;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const resp = yield fetch(uploadUrl, {
|
|
539
|
+
method: 'PUT',
|
|
540
|
+
headers,
|
|
541
|
+
body: nodeBuffer,
|
|
542
|
+
});
|
|
543
|
+
if (!resp.ok) {
|
|
544
|
+
let bodyText = '';
|
|
545
|
+
try {
|
|
546
|
+
bodyText = yield resp.text();
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
// ignore
|
|
550
|
+
}
|
|
551
|
+
throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
|
|
552
|
+
}
|
|
553
|
+
yield this.markKeyForNotDeletion(filePath);
|
|
554
|
+
if (!this.resourceConfig) {
|
|
555
|
+
throw new Error('resourceConfig is not initialized yet');
|
|
556
|
+
}
|
|
557
|
+
const { error: createError } = yield this.adminforth.createResourceRecord({
|
|
558
|
+
resource: this.resourceConfig,
|
|
559
|
+
record: Object.assign(Object.assign({}, (recordAttributes !== null && recordAttributes !== void 0 ? recordAttributes : {})), { [this.options.pathColumnName]: filePath }),
|
|
560
|
+
adminUser,
|
|
561
|
+
extra,
|
|
562
|
+
});
|
|
563
|
+
if (createError) {
|
|
564
|
+
try {
|
|
565
|
+
yield this.markKeyForDeletion(filePath);
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
// best-effort cleanup, ignore error
|
|
569
|
+
}
|
|
570
|
+
throw new Error(`Error creating record after upload: ${createError}`);
|
|
571
|
+
}
|
|
572
|
+
let previewUrl;
|
|
573
|
+
if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
|
|
574
|
+
previewUrl = this.options.preview.previewUrl({ filePath });
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
previewUrl = yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
path: filePath,
|
|
581
|
+
previewUrl,
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
}
|
|
486
585
|
}
|
package/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import { PluginOptions } from './types.js';
|
|
3
|
-
import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo } from "adminforth";
|
|
2
|
+
import { PluginOptions, UploadFromBufferParams } from './types.js';
|
|
3
|
+
import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo, RateLimiter, AdminUser, HttpExtra } from "adminforth";
|
|
4
4
|
import { Readable } from "stream";
|
|
5
|
-
import { RateLimiter } from "adminforth";
|
|
6
5
|
import { randomUUID } from "crypto";
|
|
7
6
|
import { interpretResource } from 'adminforth';
|
|
8
7
|
import { ActionCheckSource } from 'adminforth';
|
|
@@ -555,4 +554,127 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
555
554
|
}
|
|
556
555
|
|
|
557
556
|
|
|
557
|
+
/*
|
|
558
|
+
* Uploads a file from a buffer, creates a record in the resource, and returns the file path and preview URL.
|
|
559
|
+
*/
|
|
560
|
+
async uploadFromBuffer({
|
|
561
|
+
filename,
|
|
562
|
+
contentType,
|
|
563
|
+
buffer,
|
|
564
|
+
adminUser,
|
|
565
|
+
extra,
|
|
566
|
+
recordAttributes,
|
|
567
|
+
}: UploadFromBufferParams): Promise<{ path: string; previewUrl: string }> {
|
|
568
|
+
if (!filename || !contentType || !buffer) {
|
|
569
|
+
throw new Error('filename, contentType and buffer are required');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const lastDotIndex = filename.lastIndexOf('.');
|
|
573
|
+
if (lastDotIndex === -1) {
|
|
574
|
+
throw new Error('filename must contain an extension');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
|
|
578
|
+
const originalFilename = filename.substring(0, lastDotIndex);
|
|
579
|
+
|
|
580
|
+
if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let nodeBuffer: Buffer;
|
|
587
|
+
if (Buffer.isBuffer(buffer)) {
|
|
588
|
+
nodeBuffer = buffer;
|
|
589
|
+
} else if (buffer instanceof ArrayBuffer) {
|
|
590
|
+
nodeBuffer = Buffer.from(buffer);
|
|
591
|
+
} else if (ArrayBuffer.isView(buffer)) {
|
|
592
|
+
nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
593
|
+
} else {
|
|
594
|
+
throw new Error('Unsupported buffer type');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const size = nodeBuffer.byteLength;
|
|
598
|
+
if (this.options.maxFileSize && size > this.options.maxFileSize) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
const filePath: string = this.options.filePath({
|
|
604
|
+
originalFilename,
|
|
605
|
+
originalExtension,
|
|
606
|
+
contentType,
|
|
607
|
+
record: undefined,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
if (filePath.startsWith('/')) {
|
|
611
|
+
throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const { uploadUrl, uploadExtraParams } = await this.options.storageAdapter.getUploadSignedUrl(
|
|
615
|
+
filePath,
|
|
616
|
+
contentType,
|
|
617
|
+
1800,
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const headers: Record<string, string> = {
|
|
621
|
+
'Content-Type': contentType,
|
|
622
|
+
};
|
|
623
|
+
if (uploadExtraParams) {
|
|
624
|
+
Object.entries(uploadExtraParams).forEach(([key, value]) => {
|
|
625
|
+
headers[key] = value as string;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const resp = await fetch(uploadUrl as any, {
|
|
630
|
+
method: 'PUT',
|
|
631
|
+
headers,
|
|
632
|
+
body: nodeBuffer as any,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (!resp.ok) {
|
|
636
|
+
let bodyText = '';
|
|
637
|
+
try {
|
|
638
|
+
bodyText = await resp.text();
|
|
639
|
+
} catch (e) {
|
|
640
|
+
// ignore
|
|
641
|
+
}
|
|
642
|
+
throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
await this.markKeyForNotDeletion(filePath);
|
|
646
|
+
|
|
647
|
+
if (!this.resourceConfig) {
|
|
648
|
+
throw new Error('resourceConfig is not initialized yet');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const { error: createError } = await this.adminforth.createResourceRecord({
|
|
652
|
+
resource: this.resourceConfig,
|
|
653
|
+
record: { ...(recordAttributes ?? {}), [this.options.pathColumnName]: filePath },
|
|
654
|
+
adminUser,
|
|
655
|
+
extra,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
if (createError) {
|
|
659
|
+
try {
|
|
660
|
+
await this.markKeyForDeletion(filePath);
|
|
661
|
+
} catch (e) {
|
|
662
|
+
// best-effort cleanup, ignore error
|
|
663
|
+
}
|
|
664
|
+
throw new Error(`Error creating record after upload: ${createError}`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let previewUrl: string;
|
|
668
|
+
if (this.options.preview?.previewUrl) {
|
|
669
|
+
previewUrl = this.options.preview.previewUrl({ filePath });
|
|
670
|
+
} else {
|
|
671
|
+
previewUrl = await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
path: filePath,
|
|
676
|
+
previewUrl,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
558
680
|
}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdminUser, ImageGenerationAdapter, StorageAdapter } from "adminforth";
|
|
1
|
+
import { AdminUser, ImageGenerationAdapter, StorageAdapter, HttpExtra } from "adminforth";
|
|
2
2
|
|
|
3
3
|
export type PluginOptions = {
|
|
4
4
|
|
|
@@ -154,4 +154,49 @@ export type PluginOptions = {
|
|
|
154
154
|
* For now only S3 adapter is supported.
|
|
155
155
|
*/
|
|
156
156
|
storageAdapter: StorageAdapter,
|
|
157
|
-
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parameters for the UploadPlugin.uploadFromBuffer API.
|
|
161
|
+
* Used to upload a binary file buffer, create a record in the resource,
|
|
162
|
+
* and return the stored path and preview URL.
|
|
163
|
+
*/
|
|
164
|
+
export type UploadFromBufferParams = {
|
|
165
|
+
/**
|
|
166
|
+
* Original file name including extension, used to derive storage path
|
|
167
|
+
* and validate the file extension. Example: "photo.png".
|
|
168
|
+
*/
|
|
169
|
+
filename: string;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* MIME type of the uploaded file. Will be used as Content-Type
|
|
173
|
+
* when uploading to the storage adapter. Example: "image/png".
|
|
174
|
+
*/
|
|
175
|
+
contentType: string;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Binary contents of the file. Can be a Node.js Buffer, Uint8Array,
|
|
179
|
+
* or ArrayBuffer. The plugin uploads this directly without converting
|
|
180
|
+
* to base64.
|
|
181
|
+
*/
|
|
182
|
+
buffer: Buffer | Uint8Array | ArrayBuffer;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Authenticated admin user on whose behalf the record is created.
|
|
186
|
+
* Passed through to AdminForth createResourceRecord hooks.
|
|
187
|
+
*/
|
|
188
|
+
adminUser: AdminUser;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Optional HTTP context (headers, IP, etc.) forwarded to AdminForth
|
|
192
|
+
* createResourceRecord hooks. Needed to execute hooks on creation resource record for the upload.
|
|
193
|
+
*/
|
|
194
|
+
extra?: HttpExtra;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Optional additional attributes to set on the created record
|
|
198
|
+
* together with the path column managed by the plugin.
|
|
199
|
+
* Values here do NOT affect the generated storage path.
|
|
200
|
+
*/
|
|
201
|
+
recordAttributes?: Record<string, any>;
|
|
202
|
+
};
|