Skip to content

Commit

Permalink
uppy uploader
Browse files Browse the repository at this point in the history
  • Loading branch information
SanadKhan committed Sep 23, 2024
1 parent 4622c01 commit 92cd484
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 157 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ DATABASE_URL=""
REDIS_URL=""
UPLOAD_DIRECTORY=""
NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""
NEXT_PUBLIC_STORAGE_PROVIDER="local or cloudflare //default is local"
STORAGE_PROVIDER="local or cloudflare //default is local"
STRIPE_PUBLISHABLE_KEY=""
STRIPE_SECRET_KEY=""
STRIPE_SIGNING_KEY=""
Expand Down
15 changes: 15 additions & 0 deletions apps/backend/src/api/routes/media.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import handleR2Upload from '@gitroom/nestjs-libraries/upload/r2.uploader';
import { FileInterceptor } from '@nestjs/platform-express';
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import { basename } from 'path';
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';

@ApiTags('Media')
@Controller('/media')
export class MediaController {
private storage = UploadFactory.createStorage();
constructor(
private _mediaService: MediaService,
private _subscriptionService: SubscriptionService
Expand All @@ -33,6 +36,18 @@ export class MediaController {
return {output: 'data:image/png;base64,' + await this._mediaService.generateImage(prompt, org)};
}

@Post('/upload-server')
@UseInterceptors(FileInterceptor('file'))
@UsePipes(new CustomFileValidationPipe())
async uploadServer(
@GetOrgFromRequest() org: Organization,
@UploadedFile() file: Express.Multer.File
) {
const uploadedFile = await this.storage.uploadFile(file);
const filePath = uploadedFile.path.replace(process.env.UPLOAD_DIRECTORY, basename(process.env.UPLOAD_DIRECTORY));
return this._mediaService.saveFile(org.id, uploadedFile.originalname, filePath);
}

@Post('/upload-simple')
@UseInterceptors(FileInterceptor('file'))
@UsePipes(new CustomFileValidationPipe())
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/components/media/media.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export const MediaBox: FC<{
<img
className="w-full h-full object-cover"
src={mediaDirectory.set(media.path)}
alt='media'
/>
)}
</div>
Expand Down
71 changes: 17 additions & 54 deletions apps/frontend/src/components/media/new.uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
// @ts-ignore
import Uppy, { UploadResult } from '@uppy/core';
// @ts-ignore
import AwsS3Multipart from '@uppy/aws-s3-multipart';
// @ts-ignore
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';

import sha256 from 'sha256';
import { getUppyUploadPlugin } from '@gitroom/react/helpers/uppy.upload';
import { FileInput, ProgressBar } from '@uppy/react';

// Uppy styles
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';

const fetchUploadApiEndpoint = async (
fetch: any,
endpoint: string,
data: any
) => {
const res = await fetch(`/media/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
},
});

return res.json();
};

export function MultipartFileUploader({
onUploadSuccess,
allowedFileTypes,
Expand All @@ -41,7 +21,7 @@ export function MultipartFileUploader({
const [loaded, setLoaded] = useState(false);
const [reload, setReload] = useState(false);

const onUploadSuccessExtended = useCallback((result: UploadResult) => {
const onUploadSuccessExtended = useCallback((result: UploadResult<any,any>) => {
setReload(true);
onUploadSuccess(result);
}, [onUploadSuccess]);
Expand Down Expand Up @@ -78,7 +58,9 @@ export function MultipartFileUploaderAfter({
onUploadSuccess: (result: UploadResult) => void;
allowedFileTypes: string;
}) {
const storageProvider = process.env.NEXT_PUBLIC_STORAGE_PROVIDER || "local";
const fetch = useFetch();

const uppy = useMemo(() => {
const uppy2 = new Uppy({
autoProceed: true,
Expand All @@ -87,38 +69,17 @@ export function MultipartFileUploaderAfter({
allowedFileTypes: allowedFileTypes.split(','),
maxFileSize: 1000000000,
},
}).use(AwsS3Multipart, {
// @ts-ignore
createMultipartUpload: async (file) => {
const arrayBuffer = await new Response(file.data).arrayBuffer();
// @ts-ignore
const fileHash = await sha256(arrayBuffer);
const contentType = file.type;
return fetchUploadApiEndpoint(fetch, 'create-multipart-upload', {
file,
fileHash,
contentType,
});
},
// @ts-ignore
listParts: (file, props) =>
fetchUploadApiEndpoint(fetch, 'list-parts', { file, ...props }),
// @ts-ignore
signPart: (file, props) =>
fetchUploadApiEndpoint(fetch, 'sign-part', { file, ...props }),
// @ts-ignore
abortMultipartUpload: (file, props) =>
fetchUploadApiEndpoint(fetch, 'abort-multipart-upload', {
file,
...props,
}),
// @ts-ignore
completeMultipartUpload: (file, props) =>
fetchUploadApiEndpoint(fetch, 'complete-multipart-upload', {
file,
...props,
}),
});

const { plugin, options } = getUppyUploadPlugin(storageProvider, fetch)
uppy2.use(plugin, options)
// Set additional metadata when a file is added
uppy2.on('file-added', (file) => {
uppy2.setFileMeta(file.id, {
useCloudflare: storageProvider === 'cloudflare' ? 'true' : 'false', // Example of adding a custom field
// Add more fields as needed
});
});

uppy2.on('complete', (result) => {
onUploadSuccess(result);
Expand All @@ -141,15 +102,17 @@ export function MultipartFileUploaderAfter({

return (
<>
{/* <Dashboard uppy={uppy} /> */}
<ProgressBar uppy={uppy} />
<FileInput
uppy={uppy}
locale={{
strings: {
chooseFiles: 'Upload',
},
pluralize: (n) => n
}}
/>
/>
</>
);
}
66 changes: 24 additions & 42 deletions libraries/nestjs-libraries/src/upload/cloudflare.storage.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import concat from 'concat-stream';
import { StorageEngine } from 'multer';
import type { Request } from 'express';
import 'multer';
import {makeId} from "@gitroom/nestjs-libraries/services/make.is";
import mime from 'mime-types';
import { IUploadProvider } from './upload.interface';

type CallbackFunction = (
error: Error | null,
info?: Partial<Express.Multer.File>
) => void;

class CloudflareStorage implements StorageEngine {
class CloudflareStorage implements IUploadProvider {
private _client: S3Client;

public constructor(
constructor(
accountID: string,
accessKey: string,
secretKey: string,
Expand All @@ -31,56 +25,44 @@ class CloudflareStorage implements StorageEngine {
});
}

public _handleFile(
_req: Request,
file: Express.Multer.File,
callback: CallbackFunction
): void {
file.stream.pipe(
concat({ encoding: 'buffer' }, async (data) => {
// @ts-ignore
callback(null, await this._uploadFile(data, data.length, file.mimetype, mime.extension(file.mimetype)));
})
);
}

public _removeFile(
_req: Request,
file: Express.Multer.File,
callback: (error: Error | null) => void
): void {
void this._deleteFile(file.destination, callback);
}

private async _uploadFile(data: Buffer, size: number, mime: string, extension: string): Promise<Express.Multer.File> {
async uploadFile(file: Express.Multer.File): Promise<any> {
const id = makeId(10);
const extension = mime.extension(file.mimetype) || '';

// Create the PutObjectCommand to upload the file to Cloudflare R2
const command = new PutObjectCommand({
Bucket: this._bucketName,
ACL: 'public-read',
Key: `${id}.${extension}`,
Body: data,
Body: file.buffer,
});

await this._client.send(command);

return {
filename: `${id}.${extension}`,
mimetype: mime,
size,
buffer: data,
mimetype: file.mimetype,
size: file.size,
buffer: file.buffer,
originalname: `${id}.${extension}`,
fieldname: 'file',
path: `${this._uploadUrl}/${id}.${extension}`,
destination: `${this._uploadUrl}/${id}.${extension}`,
encoding: '7bit',
stream: data as any,
}
stream: file.buffer as any,
};
}

private async _deleteFile(
filedestination: string,
callback: CallbackFunction
) {
// Implement the removeFile method from IUploadProvider
async removeFile(filePath: string): Promise<void> {
// const fileName = filePath.split('/').pop(); // Extract the filename from the path

// const command = new DeleteObjectCommand({
// Bucket: this._bucketName,
// Key: fileName,
// });

// await this._client.send(command);
}
}

Expand Down
46 changes: 46 additions & 0 deletions libraries/nestjs-libraries/src/upload/local.storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { IUploadProvider } from './upload.interface';
import { mkdirSync, unlink, writeFileSync } from 'fs';
import { extname } from 'path';

export class LocalStorage implements IUploadProvider {
constructor(private uploadDirectory: string) {}

async uploadFile(file: Express.Multer.File): Promise<any> {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');

const dir = `${this.uploadDirectory}/${year}/${month}/${day}`;
mkdirSync(dir, { recursive: true });

const randomName = Array(32)
.fill(null)
.map(() => Math.round(Math.random() * 16).toString(16))
.join('');

const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
// Logic to save the file to the filesystem goes here
writeFileSync(filePath, file.buffer)

return {
filename: `${randomName}${extname(file.originalname)}`,
path: filePath,
mimetype: file.mimetype,
originalname: file.originalname
};
}

async removeFile(filePath: string): Promise<void> {
// Logic to remove the file from the filesystem goes here
return new Promise((resolve, reject) => {
unlink(filePath, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
}
25 changes: 25 additions & 0 deletions libraries/nestjs-libraries/src/upload/upload.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CloudflareStorage } from './cloudflare.storage';
import { IUploadProvider } from './upload.interface';
import { LocalStorage } from './local.storage';

export class UploadFactory {
static createStorage(): IUploadProvider {
const storageProvider = process.env.STORAGE_PROVIDER || 'local';

switch (storageProvider) {
case 'local':
return new LocalStorage(process.env.UPLOAD_DIRECTORY!);
case 'cloudflare':
return new CloudflareStorage(
process.env.CLOUDFLARE_ACCOUNT_ID!,
process.env.CLOUDFLARE_ACCESS_KEY!,
process.env.CLOUDFLARE_SECRET_ACCESS_KEY!,
process.env.CLOUDFLARE_REGION!,
process.env.CLOUDFLARE_BUCKETNAME!,
process.env.CLOUDFLARE_BUCKET_URL!
);
default:
throw new Error(`Invalid storage type ${storageProvider}`);
}
}
}
4 changes: 4 additions & 0 deletions libraries/nestjs-libraries/src/upload/upload.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IUploadProvider {
uploadFile(file: Express.Multer.File): Promise<any>;
removeFile(filePath: string): Promise<void>;
}
Loading

0 comments on commit 92cd484

Please sign in to comment.