11
2- import { PluginOptions , UploadFromBufferParams } from './types.js' ;
2+ import { PluginOptions , UploadFromBufferParams , UploadFromBufferToExistingRecordParams } from './types.js' ;
33import { AdminForthPlugin , AdminForthResourceColumn , AdminForthResource , Filters , IAdminForth , IHttpServer , suggestIfTypo , RateLimiter , AdminUser , HttpExtra } from "adminforth" ;
44import { Readable } from "stream" ;
55import { randomUUID } from "crypto" ;
@@ -549,22 +549,19 @@ export default class UploadPlugin extends AdminForthPlugin {
549549 return { error : 'failed to generate preview URL' } ;
550550 } ,
551551 } ) ;
552-
553-
554552 }
555553
556-
557554 /*
558555 * Uploads a file from a buffer, creates a record in the resource, and returns the file path and preview URL.
559556 */
560- async uploadFromBuffer ( {
557+ async uploadFromBufferToNewRecord ( {
561558 filename,
562559 contentType,
563560 buffer,
564561 adminUser,
565562 extra,
566563 recordAttributes,
567- } : UploadFromBufferParams ) : Promise < { path : string ; previewUrl : string } > {
564+ } : UploadFromBufferParams ) : Promise < { path : string ; previewUrl : string ; newRecordPk : any } > {
568565 if ( ! filename || ! contentType || ! buffer ) {
569566 throw new Error ( 'filename, contentType and buffer are required' ) ;
570567 }
@@ -648,7 +645,7 @@ export default class UploadPlugin extends AdminForthPlugin {
648645 throw new Error ( 'resourceConfig is not initialized yet' ) ;
649646 }
650647
651- const { error : createError } = await this . adminforth . createResourceRecord ( {
648+ const { error : createError , createdRecord , newRecordId } : any = await this . adminforth . createResourceRecord ( {
652649 resource : this . resourceConfig ,
653650 record : { ...( recordAttributes ?? { } ) , [ this . options . pathColumnName ] : filePath } ,
654651 adminUser,
@@ -664,6 +661,164 @@ export default class UploadPlugin extends AdminForthPlugin {
664661 throw new Error ( `Error creating record after upload: ${ createError } ` ) ;
665662 }
666663
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+
667822 let previewUrl : string ;
668823 if ( this . options . preview ?. previewUrl ) {
669824 previewUrl = this . options . preview . previewUrl ( { filePath } ) ;
0 commit comments