From 726d40b994b9a11f725a02b819af8c553f34bef5 Mon Sep 17 00:00:00 2001 From: Ammar Date: Tue, 2 Jul 2024 20:29:03 +0300 Subject: [PATCH 1/3] add: upload & download file controllers --- api-sample/package.json | 4 +- .../controllers/files/download-file/index.js | 46 ++++++++++++++ .../controllers/files/upload-file/index.js | 63 +++++++++++++++++++ .../schemas/queries/selectUserId.js | 9 +++ .../upload-file/schemas/uploadFileShema.js | 53 ++++++++++++++++ cli/utils/writeDotEnvFile.js | 6 ++ 6 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 api-sample/src/app/controllers/files/download-file/index.js create mode 100644 api-sample/src/app/controllers/files/upload-file/index.js create mode 100644 api-sample/src/app/controllers/files/upload-file/schemas/queries/selectUserId.js create mode 100644 api-sample/src/app/controllers/files/upload-file/schemas/uploadFileShema.js diff --git a/api-sample/package.json b/api-sample/package.json index d19a0b8e..ff407d62 100644 --- a/api-sample/package.json +++ b/api-sample/package.json @@ -148,6 +148,8 @@ "winston-transport": "^4.3.0", "yamlify-object": "^0.5.1", "yamlify-object-colors": "^1.0.3", - "yup": "^0.27.0" + "yup": "^0.27.0", + "@aws-sdk/client-s3": "^3.608.0", + "@aws-sdk/s3-request-presigner": "^3.608.0" } } diff --git a/api-sample/src/app/controllers/files/download-file/index.js b/api-sample/src/app/controllers/files/download-file/index.js new file mode 100644 index 00000000..8381faad --- /dev/null +++ b/api-sample/src/app/controllers/files/download-file/index.js @@ -0,0 +1,46 @@ +const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const handleAPIError = require("~root/utils/handleAPIError"); + +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY, + secretAccessKey: process.env.S3_SECRET + } +}); + +const uppyFileDownload = async (req, res) => { + const { fileUrl } = req.query; + + try { + if (!fileUrl) { + return res.status(400).json({ message: "File URL is required" }); + } + + const parsedUrl = new URL(fileUrl); + const bucketName = parsedUrl.hostname.split(".")[0]; + const fileKey = parsedUrl.pathname.substring(1); + + if (!bucketName || !fileKey) { + return res.status(400).json({ message: "Invalid file URL" }); + } + + const params = { + Bucket: bucketName, + Key: fileKey + }; + + const command = new GetObjectCommand(params); + const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); + + res.status(200).send({ + url: signedUrl, + method: "GET" + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = uppyFileDownload; diff --git a/api-sample/src/app/controllers/files/upload-file/index.js b/api-sample/src/app/controllers/files/upload-file/index.js new file mode 100644 index 00000000..32f41ff2 --- /dev/null +++ b/api-sample/src/app/controllers/files/upload-file/index.js @@ -0,0 +1,63 @@ +const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const { v4: uuidv4 } = require("uuid"); +const handleAPIError = require("~root/utils/handleAPIError"); +const uploadFileSchema = require("./schemas/uploadFileShema"); + +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY, + secretAccessKey: process.env.S3_SECRET + } +}); + +function modifyFilename(originalFilename, userId) { + const parts = originalFilename.split("."); + const fileExtension = parts.pop(); + const baseName = parts.join("."); + const uniqueId = uuidv4() + .replace(/-/g, "") + .substring(0, 10); + + const newFilename = `${userId}-${uniqueId}-${baseName}.${fileExtension}`; + + return newFilename; +} + +const uppyFileUpload = async (req, res) => { + const { userId } = req.user; + const { fileName, contentType } = req.body; + + try { + await uploadFileSchema.validate( + { userId, fileName, contentType }, + { + abortEarly: false + } + ); + + const newFileName = modifyFilename(fileName, userId); + const bucketName = process.env.S3_BUCKET_NAME; + + const params = { + Bucket: bucketName, + Key: newFileName, + ContentType: contentType, + ACL: "private" + }; + + const command = new PutObjectCommand(params); + const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); + + res.status(201).send({ + url: signedUrl, + method: "POST", + fileKey: newFileName + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = uppyFileUpload; diff --git a/api-sample/src/app/controllers/files/upload-file/schemas/queries/selectUserId.js b/api-sample/src/app/controllers/files/upload-file/schemas/queries/selectUserId.js new file mode 100644 index 00000000..556c440b --- /dev/null +++ b/api-sample/src/app/controllers/files/upload-file/schemas/queries/selectUserId.js @@ -0,0 +1,9 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserById = ({ userId }) => submitQuery` + SELECT user_id + FROM users + WHERE user_id = ${userId} +`; + +module.exports = getFirst(selectUserById, "user_id"); diff --git a/api-sample/src/app/controllers/files/upload-file/schemas/uploadFileShema.js b/api-sample/src/app/controllers/files/upload-file/schemas/uploadFileShema.js new file mode 100644 index 00000000..e057878a --- /dev/null +++ b/api-sample/src/app/controllers/files/upload-file/schemas/uploadFileShema.js @@ -0,0 +1,53 @@ +const yup = require("yup"); +const selectUserById = require("./queries/selectUserId"); + +const uploadFileSchema = yup.object().shape({ + userId: yup + .number() + .required() + .label("User ID") + .typeError("User ID must be valid.") + .test("doesUserExist", "User ID must exist", function test(userId) { + return selectUserById({ userId }).then(userFound => { + return !!userFound; + }); + }), + fileName: yup + .string() + .required() + .label("File Name") + .typeError("File Name must be valid.") + .matches( + /^[a-zA-Z0-9_.-]+$/, + "File Name can only contain letters, numbers, underscores, dashes, and periods." + ), + contentType: yup + .string() + .required() + .label("Content Type") + .typeError("Content Type must be valid.") + .oneOf( + [ + "image/jpeg", + "image/png", + "application/pdf", + "image/gif" + // "image/bmp", + // "text/plain", + // "text/csv", + // "application/msword", + // "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + // "application/vnd.ms-excel", + // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + // "application/zip", + // "application/x-rar-compressed", + // "audio/mpeg", + // "audio/wav", + // "video/mp4", + // "video/x-msvideo" + ], + "Content Type must be a valid MIME type." + ) +}); + +module.exports = uploadFileSchema; diff --git a/cli/utils/writeDotEnvFile.js b/cli/utils/writeDotEnvFile.js index 49421d3b..8bd25c23 100644 --- a/cli/utils/writeDotEnvFile.js +++ b/cli/utils/writeDotEnvFile.js @@ -22,6 +22,12 @@ MAILGUN_API_KEY=anythinggoeshere MAILGUN_API_BASE_URL=anythinggoeshere MAILGUN_DOMAIN=anythinggoeshere +// S3 credentials +AWS_REGION=anythinggoeshere +S3_ACCESS_KEY=anythinggoeshere +S3_SECRET=anythinggoeshere +S3_BUCKET_NAME=anythinggoeshere + `; const filePath = path.join(projectRoot, "./.env"); const doesItExist = fs.ensureFileSync(filePath); From b88548eea4481f061ec97d1615e793331bfd76be Mon Sep 17 00:00:00 2001 From: Ammar Hashad <60424030+ammarhashad@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:29:24 +0300 Subject: [PATCH 2/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06ac10d5..e0ecb547 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ # Quick Start ### After installing xest framework globally; you can create your api with one simple command -`xx ` +> `xx ` The above command will create your new api project then install requied packages and start your database. Please note in the project directory you will find many usefull utils, packages and middlewares. From 57dc5fe981ddf324e01797c305d9ab8e971c6ccb Mon Sep 17 00:00:00 2001 From: Ammar Hashad <60424030+ammarhashad@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:30:03 +0300 Subject: [PATCH 3/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0ecb547..cadd3ca4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ # Quick Start ### After installing xest framework globally; you can create your api with one simple command -> `xx ` +> `xx ` The above command will create your new api project then install requied packages and start your database. Please note in the project directory you will find many usefull utils, packages and middlewares.