Skip to content

Commit d70da58

Browse files
committed
feat: implement uploadFromBuffer method for file uploads
1 parent bfe2fa8 commit d70da58

1 file changed

Lines changed: 127 additions & 3 deletions

File tree

index.ts

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11

22
import { 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";
44
import { Readable } from "stream";
5-
import { RateLimiter } from "adminforth";
65

76
const 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

Comments
 (0)