@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 CHANGED
@@ -11,5 +11,5 @@ custom/preview.vue
11
11
  custom/tsconfig.json
12
12
  custom/uploader.vue
13
13
 
14
- sent 53,885 bytes received 134 bytes 108,038.00 bytes/sec
14
+ sent 53,878 bytes received 134 bytes 108,024.00 bytes/sec
15
15
  total size is 53,396 speedup is 0.99
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
4
4
  "description": "Plugin for uploading files for adminforth",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdminUser, ImageGenerationAdapter, 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
+ };