UNPKG

@lucidcms/plugin-local-storage

Version:

The official Local Storage plugin for Lucid

442 lines (423 loc) 12 kB
import fs from "fs-extra"; import path from "node:path"; import mime from "mime-types"; import { fileTypeFromFile } from "file-type"; import crypto from "node:crypto"; import { Hono } from "hono"; import { z } from "@lucidcms/core"; import { LucidAPIError, honoSwaggerParamaters, honoSwaggerResponse, serviceWrapper } from "@lucidcms/core/api"; import { createFactory } from "hono/factory"; import { describeRoute } from "hono-openapi"; import { validate } from "@lucidcms/core/middleware"; //#region src/translations/en-gb.json var file_not_found = "The file was not found."; var invalid_key = "Invalid key"; var route_localstorage_upload_error_name = "Local Storage Upload Error"; var route_localstorage_upload_error_message = "There was an error uploading the file to local storage."; var invalid_or_expired_token = "Invalid or expired token."; var invalid_file = "Invalid file."; var en_gb_default = { file_not_found, invalid_key, route_localstorage_upload_error_name, route_localstorage_upload_error_message, invalid_or_expired_token, invalid_file }; //#endregion //#region src/translations/index.ts const selectedLang = en_gb_default; const T = (key, data) => { const translation = selectedLang[key]; if (!translation) return key; if (!data) return translation; return translation.replace(/\{\{(\w+)\}\}/g, (_, p1) => data[p1]); }; var translations_default = T; //#endregion //#region src/utils/helpers.ts const keyPaths = (key, uploadDir) => { const keyPath = key.split("/").slice(0, -1).join("/"); const filename = key.split("/").pop(); if (!filename) throw new Error(translations_default("invalid_key")); const targetDir = path.join(uploadDir, keyPath); const targetPath = path.join(targetDir, filename); return { keyPath, filename, targetDir, targetPath }; }; //#endregion //#region src/services/steam.ts var steam_default = (pluginOptions) => { const stream = async (key, options) => { try { const { targetPath } = keyPaths(key, pluginOptions.uploadDir); const exists = await fs.pathExists(targetPath); if (!exists) return { error: { message: translations_default("file_not_found"), status: 404 }, data: void 0 }; const [stats, fileTypeResult] = await Promise.all([fs.stat(targetPath), fileTypeFromFile(targetPath)]); let mimeType; const totalSize = stats.size; if (fileTypeResult) mimeType = fileTypeResult.mime; else { const fileExtension = path.extname(targetPath); mimeType = mime.lookup(fileExtension) || void 0; if (mimeType === "application/mp4") mimeType = "video/mp4"; } if (options?.range) { const start = options.range.start; const end = options.range.end ?? totalSize - 1; if (start >= totalSize || end >= totalSize || start > end) return { error: { message: "Invalid range", status: 416 }, data: void 0 }; const body$1 = fs.createReadStream(targetPath, { start, end }); const contentLength = end - start + 1; return { error: void 0, data: { contentLength, contentType: mimeType || void 0, body: body$1, isPartialContent: true, totalSize, range: { start, end } } }; } const body = fs.createReadStream(targetPath); return { error: void 0, data: { contentLength: totalSize, contentType: mimeType || void 0, body, isPartialContent: false, totalSize } }; } catch (e) { const error = e; return { error: { message: error.message, status: 500 }, data: void 0 }; } }; return stream; }; //#endregion //#region src/services/delete-single.ts var delete_single_default = (pluginOptions) => { const deletSingle = async (key) => { try { const { targetPath } = keyPaths(key, pluginOptions.uploadDir); const exists = await fs.pathExists(targetPath); if (!exists) return { error: { message: translations_default("file_not_found") }, data: void 0 }; await fs.unlink(targetPath); return { error: void 0, data: void 0 }; } catch (e) { const error = e; return { error: { message: error.message }, data: void 0 }; } }; return deletSingle; }; //#endregion //#region src/services/delete-multiple.ts var delete_multiple_default = (pluginOptions) => { const deleteMultiple = async (keys) => { try { for (const key of keys) { const { targetPath } = keyPaths(key, pluginOptions.uploadDir); const exists = await fs.pathExists(targetPath); if (!exists) continue; await fs.unlink(targetPath); } return { error: void 0, data: void 0 }; } catch (e) { const error = e; return { error: { message: error.message }, data: void 0 }; } }; return deleteMultiple; }; //#endregion //#region src/services/upload-single.ts var upload_single_default = (pluginOptions) => { const uploadSingle$1 = async (props) => { try { const { targetDir, targetPath } = keyPaths(props.key, pluginOptions.uploadDir); await fs.ensureDir(targetDir); if (Buffer.isBuffer(props.data)) await fs.writeFile(targetPath, props.data); else { const writeStream = fs.createWriteStream(targetPath); props.data.pipe(writeStream); await new Promise((resolve, reject) => { writeStream.on("finish", resolve); writeStream.on("error", reject); }); } return { error: void 0, data: {} }; } catch (e) { const error = e; return { error: { message: error.message }, data: void 0 }; } }; return uploadSingle$1; }; //#endregion //#region src/services/get-presigned-url.ts var get_presigned_url_default = (pluginOptions) => { const getPresignedUrl = async (key, meta) => { try { const timestamp = Date.now(); const token = crypto.createHmac("sha256", pluginOptions.secretKey).update(`${key}${timestamp}`).digest("hex"); return { error: void 0, data: { url: `${meta.host}/api/v1/localstorage/upload?key=${key}&token=${token}&timestamp=${timestamp}` } }; } catch (e) { const error = e; return { error: { message: error.message, status: 500 }, data: void 0 }; } }; return getPresignedUrl; }; //#endregion //#region src/services/get-metadata.ts var get_metadata_default = (pluginOptions) => { const getMetadata = async (key) => { try { const { targetPath } = keyPaths(key, pluginOptions.uploadDir); const exists = await fs.pathExists(targetPath); if (!exists) return { error: { message: translations_default("file_not_found"), status: 404 }, data: void 0 }; const [stats, fileTypeResult] = await Promise.all([fs.stat(targetPath), fileTypeFromFile(targetPath)]); let mimeType = null; if (fileTypeResult) mimeType = fileTypeResult.mime; else { const fileExtension = path.extname(targetPath); mimeType = mime.lookup(fileExtension) || null; if (mimeType === "application/mp4") mimeType = "video/mp4"; } const etag = crypto.createHash("md5").update(`${stats.mtime.getTime()}-${stats.size}`).digest("hex"); return { error: void 0, data: { size: stats.size, mimeType, etag } }; } catch (e) { const error = e; return { error: { message: error.message, status: 500 }, data: void 0 }; } }; return getMetadata; }; //#endregion //#region src/schema/upload.ts const controllerSchemas = { upload: { body: void 0, query: { string: z.object({ token: z.string().meta({ description: "The presigned URL token", example: "a64825f15c2acd40f8865933a26b7334d2c3dec3aba483cfab17396da0be8abe" }), timestamp: z.string().meta({ description: "Timestamp", example: "1745601807970" }), key: z.string().meta({ description: "The media key", example: "2024/09/5ttogd-placeholder-image.png" }) }), formatted: void 0 }, params: void 0, response: void 0 } }; //#endregion //#region src/constants.ts const PLUGIN_KEY = "plugin-local-storage"; const LUCID_VERSION = "0.x.x"; const PRESIGNED_URL_EXPIRY = 36e5; //#endregion //#region src/services/checks/validate-presigned-token.ts const validatePresignedToken = async (_, data) => { const expectedToken = crypto.createHmac("sha256", data.pluginOptions.secretKey).update(`${data.key}${data.timestamp}`).digest("hex"); if (data.token !== expectedToken || Date.now() - Number.parseInt(data.timestamp) > PRESIGNED_URL_EXPIRY) return { error: { status: 403, type: "basic", message: translations_default("invalid_or_expired_token") }, data: void 0 }; return { error: void 0, data: void 0 }; }; var validate_presigned_token_default = validatePresignedToken; //#endregion //#region src/services/checks/index.ts var checks_default = { validatePresignedToken: validate_presigned_token_default }; //#endregion //#region src/services/upload-single-endpoint.ts const uploadSingle = async (context, data) => { const checkPresignedTokenRes = await checks_default.validatePresignedToken(context, { pluginOptions: data.pluginOptions, key: data.key, token: data.token, timestamp: data.timestamp }); if (checkPresignedTokenRes.error) return checkPresignedTokenRes; const { targetDir, targetPath } = keyPaths(data.key, data.pluginOptions.uploadDir); await fs.ensureDir(targetDir); if (Buffer.isBuffer(data.buffer)) await fs.writeFile(targetPath, data.buffer); else return { error: { type: "basic", status: 400, message: translations_default("invalid_file") } }; return { error: void 0, data: true }; }; var upload_single_endpoint_default = uploadSingle; //#endregion //#region src/controllers/upload.ts const factory = createFactory(); const uploadController = (pluginOptions) => factory.createHandlers(describeRoute({ description: "Upload a single media file.", tags: ["localstorage-plugin"], summary: "Upload File", parameters: honoSwaggerParamaters({ query: controllerSchemas.upload.query.string }), responses: honoSwaggerResponse({ noProperties: true }), validateResponse: true }), validate("query", controllerSchemas.upload.query.string), async (c) => { const query = c.req.valid("query"); const buffer = await c.req.arrayBuffer(); const uploadMedia = await serviceWrapper(upload_single_endpoint_default, { transaction: false, defaultError: { type: "basic", name: translations_default("route_localstorage_upload_error_name"), message: translations_default("route_localstorage_upload_error_message") } })({ db: c.get("config").db.client, config: c.get("config"), services: void 0 }, { buffer: buffer ? Buffer.from(buffer) : void 0, key: query.key, token: query.token, timestamp: query.timestamp, pluginOptions }); if (uploadMedia.error) throw new LucidAPIError(uploadMedia.error); c.status(200); return c.body(null); }); var upload_default = uploadController; //#endregion //#region src/routes/index.ts const routes = (pluginOptions) => async (app) => { const localStorageRoutes = new Hono().put("/api/v1/localstorage/upload", ...upload_default(pluginOptions)); app.route("/", localStorageRoutes); }; var routes_default = routes; //#endregion //#region src/plugin.ts const plugin = async (config, pluginOptions) => { config.hono.extensions?.push(routes_default(pluginOptions)); config.media = { ...config.media, strategy: { getPresignedUrl: get_presigned_url_default(pluginOptions), getMeta: get_metadata_default(pluginOptions), stream: steam_default(pluginOptions), uploadSingle: upload_single_default(pluginOptions), deleteSingle: delete_single_default(pluginOptions), deleteMultiple: delete_multiple_default(pluginOptions) } }; return { key: PLUGIN_KEY, lucid: LUCID_VERSION, config }; }; var plugin_default = plugin; //#endregion //#region src/index.ts const lucidLocalStorage = (pluginOptions) => (config) => plugin_default(config, pluginOptions); var src_default = lucidLocalStorage; //#endregion export { src_default as default }; //# sourceMappingURL=index.js.map