src for upload file to S3 object storage
This commit is contained in:
3
upload-large-file/.gitignore
vendored
Normal file
3
upload-large-file/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
src/be/uploads
|
||||||
|
.env
|
||||||
2938
upload-large-file/package-lock.json
generated
Normal file
2938
upload-large-file/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
upload-large-file/package.json
Normal file
21
upload-large-file/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "upload-large-file",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start:api": "node src/be/index.js",
|
||||||
|
"start:client": "http-server src/fe -p 8080"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "3.670.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "3.670.0",
|
||||||
|
"aws-sdk": "2.1691.0",
|
||||||
|
"cors": "2.8.5",
|
||||||
|
"dotenv": "16.4.5",
|
||||||
|
"express": "4.21.1",
|
||||||
|
"multer": "1.4.5-lts.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
upload-large-file/src/be/generate-presigned-url-handler.js
Normal file
42
upload-large-file/src/be/generate-presigned-url-handler.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const {
|
||||||
|
S3Client,
|
||||||
|
PutObjectCommand,
|
||||||
|
PutBucketCorsCommand,
|
||||||
|
} = require("@aws-sdk/client-s3");
|
||||||
|
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||||
|
|
||||||
|
const s3Client = new S3Client({
|
||||||
|
region: "auto",
|
||||||
|
endpoint: process.env.R2_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const generatePresignedUrlHandler = async (_req, res) => {
|
||||||
|
const targetBucket = "test-large-files";
|
||||||
|
const fileKey = `${targetBucket}/${Date.now()}-${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(7)}`;
|
||||||
|
const expiresIn = 60 * 60; // 1 hour expiration
|
||||||
|
|
||||||
|
try {
|
||||||
|
const presignedUrl = await getPresignedUrlForUpload(fileKey, expiresIn);
|
||||||
|
res.json({ url: presignedUrl });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating pre-signed URL", error);
|
||||||
|
res.status(500).json({ error: "Failed to generate pre-signed URL" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getPresignedUrlForUpload(key, expiresIn) {
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: process.env.R2_BUCKET_NAME,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await getSignedUrl(s3Client, command, { expiresIn });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generatePresignedUrlHandler };
|
||||||
60
upload-large-file/src/be/index.js
Normal file
60
upload-large-file/src/be/index.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
require("dotenv").config();
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const {
|
||||||
|
generatePresignedUrlHandler,
|
||||||
|
enableBucketCorsHandler,
|
||||||
|
} = require("./generate-presigned-url-handler");
|
||||||
|
const { uploadFileHandler } = require("./upload-file-handler");
|
||||||
|
const upload = require("./upload-middleware");
|
||||||
|
const {
|
||||||
|
initiateMultiPartsHandler,
|
||||||
|
getPresignedUrlHandler,
|
||||||
|
completeMultiPartsHandler,
|
||||||
|
} = require("./multi-part-handler");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// NOTE: The presigned URL routes
|
||||||
|
app.post("/generate-presigned-url", generatePresignedUrlHandler);
|
||||||
|
|
||||||
|
// NOTE: The multipart upload routes
|
||||||
|
app.post("/initiate-multipart-upload", initiateMultiPartsHandler);
|
||||||
|
app.post("/get-presigned-url", getPresignedUrlHandler);
|
||||||
|
app.post("/complete-multipart-upload", completeMultiPartsHandler);
|
||||||
|
|
||||||
|
app.post("/enable-bucket-cors", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await s3Client.send(
|
||||||
|
new PutBucketCorsCommand({
|
||||||
|
Bucket: process.env.R2_BUCKET_NAME,
|
||||||
|
CORSConfiguration: {
|
||||||
|
CORSRules: [
|
||||||
|
{
|
||||||
|
AllowedHeaders: ["*"],
|
||||||
|
AllowedMethods: ["GET", "PUT", "HEAD", "POST", "OPTIONS"],
|
||||||
|
AllowedOrigins: ["*"],
|
||||||
|
ExposeHeaders: ["ETag"],
|
||||||
|
MaxAgeSeconds: 3000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log("CORS configuration set successfully:", response);
|
||||||
|
res.json({ response });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting CORS configuration:", error);
|
||||||
|
res.status(500).json({ error: "Failed to set CORS configuration" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/upload", upload.single("video"), uploadFileHandler);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
72
upload-large-file/src/be/multi-part-handler.js
Normal file
72
upload-large-file/src/be/multi-part-handler.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const {
|
||||||
|
CreateMultipartUploadCommand,
|
||||||
|
UploadPartCommand,
|
||||||
|
S3Client,
|
||||||
|
CompleteMultipartUploadCommand,
|
||||||
|
} = require("@aws-sdk/client-s3");
|
||||||
|
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||||
|
|
||||||
|
const s3Client = new S3Client({
|
||||||
|
region: "auto",
|
||||||
|
endpoint: process.env.R2_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetFolder = "test-large-files";
|
||||||
|
|
||||||
|
const initiateMultiPartsHandler = async (req, res) => {
|
||||||
|
const { fileName } = req.body;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: process.env.R2_BUCKET_NAME,
|
||||||
|
Key: `${targetFolder}/${fileName}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const command = new CreateMultipartUploadCommand(params);
|
||||||
|
const { UploadId } = await s3Client.send(command);
|
||||||
|
|
||||||
|
res.json({ uploadId: UploadId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPresignedUrlHandler = async (req, res) => {
|
||||||
|
const { uploadId, partNumber, fileName } = req.body;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: process.env.R2_BUCKET_NAME,
|
||||||
|
Key: `${targetFolder}/${fileName}`,
|
||||||
|
PartNumber: partNumber,
|
||||||
|
UploadId: uploadId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const command = new UploadPartCommand(params);
|
||||||
|
const presignedUrl = await getSignedUrl(s3Client, command, {
|
||||||
|
expiresIn: 3600,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ url: presignedUrl });
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeMultiPartsHandler = async (req, res) => {
|
||||||
|
const { uploadId, parts, fileName } = req.body;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: process.env.R2_BUCKET_NAME,
|
||||||
|
Key: `${targetFolder}/${fileName}`,
|
||||||
|
UploadId: uploadId,
|
||||||
|
MultipartUpload: { Parts: parts },
|
||||||
|
};
|
||||||
|
|
||||||
|
const command = new CompleteMultipartUploadCommand(params);
|
||||||
|
await s3Client.send(command);
|
||||||
|
|
||||||
|
res.json({ message: "Upload completed" });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initiateMultiPartsHandler,
|
||||||
|
getPresignedUrlHandler,
|
||||||
|
completeMultiPartsHandler,
|
||||||
|
};
|
||||||
45
upload-large-file/src/be/upload-file-handler.js
Normal file
45
upload-large-file/src/be/upload-file-handler.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const AWS = require("aws-sdk");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const s3 = new AWS.S3({
|
||||||
|
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
||||||
|
endpoint: process.env.R2_ENDPOINT,
|
||||||
|
signatureVersion: "v4",
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadFileHandler = async (req, res) => {
|
||||||
|
const file = req.file;
|
||||||
|
if (!file) return res.status(400).json({ message: "No file uploaded" });
|
||||||
|
const fileStream = fs.createReadStream(file.path);
|
||||||
|
|
||||||
|
// NOTE: This is the simplest way to upload a file to S3.
|
||||||
|
const uploadParams = {
|
||||||
|
Bucket: process.env.R2_BUCKET_NAME,
|
||||||
|
Key: file.filename,
|
||||||
|
Body: fileStream,
|
||||||
|
ContentType: file.mimetype,
|
||||||
|
};
|
||||||
|
|
||||||
|
const multipartUploadParams = {
|
||||||
|
Bucket: process.env.R2_BUCKET_NAME,
|
||||||
|
Key: `uploads/test-mootod/${file.filename}`,
|
||||||
|
ContentType: file.mimetype,
|
||||||
|
PartSize: 10 * 1024 * 1024, // 10MB per part
|
||||||
|
Body: fileStream,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await s3.upload(multipartUploadParams).promise();
|
||||||
|
res.json({ message: "File uploaded successfully", url: data.Location });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading file:", error);
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ message: "File upload failed", error: error.message });
|
||||||
|
} finally {
|
||||||
|
fs.unlinkSync(file.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { uploadFileHandler };
|
||||||
15
upload-large-file/src/be/upload-middleware.js
Normal file
15
upload-large-file/src/be/upload-middleware.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const multer = require("multer");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, path.join(__dirname, "uploads"));
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
cb(null, Date.now() + path.extname(file.originalname));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({ storage });
|
||||||
|
|
||||||
|
module.exports = upload;
|
||||||
133
upload-large-file/src/fe/index.html
Normal file
133
upload-large-file/src/fe/index.html
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Upload Video to R2</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Upload a Video File</h1>
|
||||||
|
<form id="uploadForm" enctype="multipart/form-data">
|
||||||
|
<input type="file" id="fileInput" />
|
||||||
|
<button type="submit">Upload</button>
|
||||||
|
</form>
|
||||||
|
<progress id="progressBar" value="0" max="100"></progress>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document
|
||||||
|
.getElementById("uploadForm")
|
||||||
|
.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await uploadFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function uploadFile() {
|
||||||
|
const fileInput = document.getElementById("fileInput");
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) {
|
||||||
|
alert("Please select a file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadId = await initiateMultipartUpload(file.name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: The maximum part size is 5 GB. For simplicity, we are using 500 MB here.
|
||||||
|
* Refer to the S3 documentation for more information: https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html
|
||||||
|
*/
|
||||||
|
const partSize = 500 * 1024 * 1024; // 500 MB per part
|
||||||
|
const parts = [];
|
||||||
|
const uploadPromises = [];
|
||||||
|
let uploadedSize = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < file.size; i += partSize) {
|
||||||
|
const partNumber = Math.floor(i / partSize) + 1;
|
||||||
|
const chunk = file.slice(i, i + partSize);
|
||||||
|
|
||||||
|
const uploadPromise = (async (partNumber) => {
|
||||||
|
const presignedUrl = await getPresignedUrl(
|
||||||
|
uploadId,
|
||||||
|
partNumber,
|
||||||
|
file.name
|
||||||
|
);
|
||||||
|
|
||||||
|
const etag = await uploadPartToS3(presignedUrl, chunk);
|
||||||
|
|
||||||
|
parts.push({
|
||||||
|
PartNumber: partNumber,
|
||||||
|
ETag: etag,
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadedSize += chunk.size;
|
||||||
|
const progressBarEle = document.getElementById("progressBar");
|
||||||
|
progressBarEle.value = (uploadedSize / file.size) * 100;
|
||||||
|
})(partNumber);
|
||||||
|
|
||||||
|
uploadPromises.push(uploadPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(uploadPromises);
|
||||||
|
await completeMultipartUpload(uploadId, parts, file.name);
|
||||||
|
alert("File uploaded successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initiateMultipartUpload(fileName) {
|
||||||
|
const response = await fetch(
|
||||||
|
"http://localhost:3000/initiate-multipart-upload",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ fileName }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { uploadId } = await response.json();
|
||||||
|
return uploadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPresignedUrl(uploadId, partNumber, fileName) {
|
||||||
|
const response = await fetch(
|
||||||
|
"http://localhost:3000/get-presigned-url",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ uploadId, partNumber, fileName }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { url } = await response.json();
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadPartToS3(url, chunk) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
},
|
||||||
|
body: chunk,
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Failed to upload part");
|
||||||
|
const etag = `${response.headers.get("ETag")}`.replace(/"/g, "");
|
||||||
|
|
||||||
|
if (!etag) {
|
||||||
|
throw new Error("Failed to retrieve ETag from the response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return etag;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeMultipartUpload(uploadId, parts, fileName) {
|
||||||
|
await fetch("http://localhost:3000/complete-multipart-upload", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ uploadId, parts, fileName }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user