11
22import { PluginOptions } from './types.js' ;
3- import { AdminForthPlugin , AdminForthResourceColumn , AdminForthResource , Filters , IAdminForth , IHttpServer , suggestIfTypo } from "adminforth" ;
3+ import { AdminForthPlugin , AdminForthResourceColumn , AdminForthResource , Filters , IAdminForth , IHttpServer , suggestIfTypo , RateLimiter , AdminUser , HttpExtra } from "adminforth" ;
44import { Readable } from "stream" ;
5- import { RateLimiter } from "adminforth" ;
65
76const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup' ;
87
@@ -284,7 +283,7 @@ export default class UploadPlugin extends AdminForthPlugin {
284283 method : 'POST' ,
285284 path : `/plugin/${ this . pluginInstanceId } /get_file_upload_url` ,
286285 handler : async ( { body } ) => {
287- const { originalFilename, contentType, size , originalExtension, recordPk } = body ;
286+ const { originalFilename, contentType, originalExtension, recordPk } = body ;
288287
289288 if ( this . options . allowedFileExtensions && ! this . options . allowedFileExtensions . includes ( originalExtension . toLowerCase ( ) ) ) {
290289 return {
@@ -463,4 +462,129 @@ export default class UploadPlugin extends AdminForthPlugin {
463462
464463 }
465464
465+ async uploadFromBuffer ( {
466+ filename,
467+ contentType,
468+ buffer,
469+ adminUser,
470+ extra,
471+ } : {
472+ filename : string ;
473+ contentType : string ;
474+ buffer : Buffer | Uint8Array | ArrayBuffer ;
475+ adminUser : AdminUser ;
476+ extra ?: HttpExtra ;
477+ } ) : Promise < { path : string ; previewUrl : string } > {
478+ if ( ! filename || ! contentType || ! buffer ) {
479+ throw new Error ( 'filename, contentType and buffer are required' ) ;
480+ }
481+
482+ const lastDotIndex = filename . lastIndexOf ( '.' ) ;
483+ if ( lastDotIndex === - 1 ) {
484+ throw new Error ( 'filename must contain an extension' ) ;
485+ }
486+
487+ const originalExtension = filename . substring ( lastDotIndex + 1 ) . toLowerCase ( ) ;
488+ const originalFilename = filename . substring ( 0 , lastDotIndex ) ;
489+
490+ if ( this . options . allowedFileExtensions && ! this . options . allowedFileExtensions . includes ( originalExtension ) ) {
491+ throw new Error (
492+ `File extension "${ originalExtension } " is not allowed, allowed extensions are: ${ this . options . allowedFileExtensions . join ( ', ' ) } `
493+ ) ;
494+ }
495+
496+ let nodeBuffer : Buffer ;
497+ if ( Buffer . isBuffer ( buffer ) ) {
498+ nodeBuffer = buffer ;
499+ } else if ( buffer instanceof ArrayBuffer ) {
500+ nodeBuffer = Buffer . from ( buffer ) ;
501+ } else if ( ArrayBuffer . isView ( buffer ) ) {
502+ nodeBuffer = Buffer . from ( buffer . buffer , buffer . byteOffset , buffer . byteLength ) ;
503+ } else {
504+ throw new Error ( 'Unsupported buffer type' ) ;
505+ }
506+
507+ const size = nodeBuffer . byteLength ;
508+ if ( this . options . maxFileSize && size > this . options . maxFileSize ) {
509+ throw new Error (
510+ `File size ${ size } is too large. Maximum allowed size is ${ this . options . maxFileSize } `
511+ ) ;
512+ }
513+ const filePath : string = this . options . filePath ( {
514+ originalFilename,
515+ originalExtension,
516+ contentType,
517+ record : undefined ,
518+ } ) ;
519+
520+ if ( filePath . startsWith ( '/' ) ) {
521+ throw new Error ( 's3Path should not start with /, please adjust s3path function to not return / at the start of the path' ) ;
522+ }
523+
524+ const { uploadUrl, uploadExtraParams } = await this . options . storageAdapter . getUploadSignedUrl (
525+ filePath ,
526+ contentType ,
527+ 1800 ,
528+ ) ;
529+
530+ const headers : Record < string , string > = {
531+ 'Content-Type' : contentType ,
532+ } ;
533+ if ( uploadExtraParams ) {
534+ Object . entries ( uploadExtraParams ) . forEach ( ( [ key , value ] ) => {
535+ headers [ key ] = value as string ;
536+ } ) ;
537+ }
538+
539+ const resp = await fetch ( uploadUrl as any , {
540+ method : 'PUT' ,
541+ headers,
542+ body : nodeBuffer as any ,
543+ } ) ;
544+
545+ if ( ! resp . ok ) {
546+ let bodyText = '' ;
547+ try {
548+ bodyText = await resp . text ( ) ;
549+ } catch ( e ) {
550+ // ignore
551+ }
552+ throw new Error ( `Upload failed with status ${ resp . status } : ${ bodyText } ` ) ;
553+ }
554+
555+ await this . options . storageAdapter . markKeyForNotDeletation ( filePath ) ;
556+
557+ if ( ! this . resourceConfig ) {
558+ throw new Error ( 'resourceConfig is not initialized yet' ) ;
559+ }
560+
561+ const { error : createError } = await this . adminforth . createResourceRecord ( {
562+ resource : this . resourceConfig ,
563+ record : { [ this . options . pathColumnName ] : filePath } ,
564+ adminUser,
565+ extra,
566+ } ) ;
567+
568+ if ( createError ) {
569+ try {
570+ await this . options . storageAdapter . markKeyForDeletation ( filePath ) ;
571+ } catch ( e ) {
572+ // best-effort cleanup, ignore error
573+ }
574+ throw new Error ( `Error creating record after upload: ${ createError } ` ) ;
575+ }
576+
577+ let previewUrl : string ;
578+ if ( this . options . preview ?. previewUrl ) {
579+ previewUrl = this . options . preview . previewUrl ( { filePath } ) ;
580+ } else {
581+ previewUrl = await this . options . storageAdapter . getDownloadUrl ( filePath , 1800 ) ;
582+ }
583+
584+ return {
585+ path : filePath ,
586+ previewUrl,
587+ } ;
588+ }
589+
466590}
0 commit comments