Skip to content

Commit 7f1cf0c

Browse files
committed
feat: add uploadFromBufferToExistingRecord
1 parent 7016d4e commit 7f1cf0c

File tree

2 files changed

+175
-7
lines changed

2 files changed

+175
-7
lines changed

index.ts

Lines changed: 162 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
import { PluginOptions, UploadFromBufferParams } from './types.js';
2+
import { PluginOptions, UploadFromBufferParams, UploadFromBufferToExistingRecordParams } from './types.js';
33
import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo, RateLimiter, AdminUser, HttpExtra } from "adminforth";
44
import { Readable } from "stream";
55
import { 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 });

types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,17 @@ export type UploadFromBufferParams = {
199199
* Values here do NOT affect the generated storage path.
200200
*/
201201
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;
202215
};

0 commit comments

Comments
 (0)