@lucidcms/plugin-local-storage
Version:
The official Local Storage plugin for Lucid
442 lines (423 loc) • 12 kB
JavaScript
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}×tamp=${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