219 lines
5.7 KiB
TypeScript
219 lines
5.7 KiB
TypeScript
import {
|
|
UploadPartCommand, S3Client, ListPartsCommand, CreateMultipartUploadCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand
|
|
} from '@aws-sdk/client-s3';
|
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
import { Request, Response } from 'express';
|
|
import crypto from 'crypto';
|
|
import path from 'path';
|
|
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|
|
|
const { CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ACCESS_KEY, CLOUDFLARE_SECRET_ACCESS_KEY, CLOUDFLARE_BUCKETNAME, CLOUDFLARE_BUCKET_URL } =
|
|
process.env;
|
|
|
|
const R2 = new S3Client({
|
|
region: 'auto',
|
|
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
credentials: {
|
|
accessKeyId: CLOUDFLARE_ACCESS_KEY!,
|
|
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY!,
|
|
},
|
|
});
|
|
|
|
// Function to generate a random string
|
|
function generateRandomString() {
|
|
return makeId(20);
|
|
}
|
|
|
|
export default async function handleR2Upload(
|
|
endpoint: string,
|
|
req: Request,
|
|
res: Response
|
|
) {
|
|
switch (endpoint) {
|
|
case 'create-multipart-upload':
|
|
return createMultipartUpload(req, res);
|
|
case 'prepare-upload-parts':
|
|
return prepareUploadParts(req, res);
|
|
case 'complete-multipart-upload':
|
|
return completeMultipartUpload(req, res);
|
|
case 'list-parts':
|
|
return listParts(req, res);
|
|
case 'abort-multipart-upload':
|
|
return abortMultipartUpload(req, res);
|
|
case 'sign-part':
|
|
return signPart(req, res);
|
|
}
|
|
return res.status(404).end();
|
|
}
|
|
|
|
export async function simpleUpload(data: Buffer, originalFilename: string, contentType: string) {
|
|
const fileExtension = path.extname(originalFilename); // Extract extension
|
|
const randomFilename = generateRandomString() + fileExtension; // Append extension
|
|
|
|
const params = {
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: randomFilename,
|
|
Body: data,
|
|
ContentType: contentType,
|
|
};
|
|
|
|
const command = new PutObjectCommand({ ...params });
|
|
await R2.send(command);
|
|
|
|
return CLOUDFLARE_BUCKET_URL + '/' + randomFilename;
|
|
}
|
|
|
|
export async function createMultipartUpload(
|
|
req: Request,
|
|
res: Response
|
|
) {
|
|
const { file, fileHash, contentType } = req.body;
|
|
const fileExtension = path.extname(file.name); // Extract extension
|
|
const randomFilename = generateRandomString() + fileExtension; // Append extension
|
|
|
|
try {
|
|
const params = {
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: `${randomFilename}`,
|
|
ContentType: contentType,
|
|
Metadata: {
|
|
'x-amz-meta-file-hash': fileHash,
|
|
},
|
|
};
|
|
|
|
const command = new CreateMultipartUploadCommand({ ...params });
|
|
const response = await R2.send(command);
|
|
return res.status(200).json({
|
|
uploadId: response.UploadId,
|
|
key: response.Key,
|
|
});
|
|
} catch (err) {
|
|
console.log('Error', err);
|
|
return res.status(500).json({ source: { status: 500 } });
|
|
}
|
|
}
|
|
|
|
export async function prepareUploadParts(
|
|
req: Request,
|
|
res: Response
|
|
) {
|
|
const { partData } = req.body;
|
|
|
|
const parts = partData.parts;
|
|
|
|
const response = {
|
|
presignedUrls: {},
|
|
};
|
|
|
|
for (const part of parts) {
|
|
try {
|
|
const params = {
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: partData.key,
|
|
PartNumber: part.number,
|
|
UploadId: partData.uploadId,
|
|
};
|
|
const command = new UploadPartCommand({ ...params });
|
|
const url = await getSignedUrl(R2, command, { expiresIn: 3600 });
|
|
|
|
// @ts-ignore
|
|
response.presignedUrls[part.number] = url;
|
|
} catch (err) {
|
|
console.log('Error', err);
|
|
return res.status(500).json(err);
|
|
}
|
|
}
|
|
|
|
return res.status(200).json(response);
|
|
}
|
|
|
|
export async function listParts(req: Request, res: Response) {
|
|
const { key, uploadId } = req.body;
|
|
|
|
try {
|
|
const params = {
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: key,
|
|
UploadId: uploadId,
|
|
};
|
|
const command = new ListPartsCommand({ ...params });
|
|
const response = await R2.send(command);
|
|
|
|
return res.status(200).json(response['Parts']);
|
|
} catch (err) {
|
|
console.log('Error', err);
|
|
return res.status(500).json(err);
|
|
}
|
|
}
|
|
|
|
export async function completeMultipartUpload(
|
|
req: Request,
|
|
res: Response
|
|
) {
|
|
const { key, uploadId, parts } = req.body;
|
|
|
|
try {
|
|
const params = {
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: key,
|
|
UploadId: uploadId,
|
|
MultipartUpload: { Parts: parts },
|
|
};
|
|
|
|
const command = new CompleteMultipartUploadCommand({
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: key,
|
|
UploadId: uploadId,
|
|
MultipartUpload: { Parts: parts },
|
|
});
|
|
const response = await R2.send(command);
|
|
response.Location = process.env.CLOUDFLARE_BUCKET_URL + '/' + response?.Location?.split('/').at(-1);
|
|
return response;
|
|
} catch (err) {
|
|
console.log('Error', err);
|
|
return res.status(500).json(err);
|
|
}
|
|
}
|
|
|
|
export async function abortMultipartUpload(
|
|
req: Request,
|
|
res: Response
|
|
) {
|
|
const { key, uploadId } = req.body;
|
|
|
|
try {
|
|
const params = {
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: key,
|
|
UploadId: uploadId,
|
|
};
|
|
const command = new AbortMultipartUploadCommand({ ...params });
|
|
const response = await R2.send(command);
|
|
|
|
return res.status(200).json(response);
|
|
} catch (err) {
|
|
console.log('Error', err);
|
|
return res.status(500).json(err);
|
|
}
|
|
}
|
|
|
|
export async function signPart(req: Request, res: Response) {
|
|
const { key, uploadId } = req.body;
|
|
const partNumber = parseInt(req.body.partNumber);
|
|
|
|
const params = {
|
|
Bucket: CLOUDFLARE_BUCKETNAME,
|
|
Key: key,
|
|
PartNumber: partNumber,
|
|
UploadId: uploadId,
|
|
Expires: 3600
|
|
};
|
|
|
|
const command = new UploadPartCommand({ ...params });
|
|
const url = await getSignedUrl(R2, command, { expiresIn: 3600 });
|
|
|
|
return res.status(200).json({
|
|
url: url,
|
|
});
|
|
}
|