UNPKG

@medusajs/file-local

Version:

Local filesystem file storage for Medusa

189 lines • 8.77 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalFileService = void 0; const utils_1 = require("@medusajs/framework/utils"); const fs_1 = require("fs"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); /** * Decodes the string `content` of an uploaded file into a Buffer. * * Upload inputs arrive as a string that may be base64, UTF-8 text (e.g. a CSV * with special characters, see #13649) or a binary/latin1 string (e.g. an image * produced via `buffer.toString("binary")`, as the upload docs instruct). * Decoding a binary string as UTF-8 corrupts every byte > 127 (a PNG's leading * `0x89` becomes `0xC2 0x89`), so the encoding is chosen from the file's MIME * type, defaulting to binary for non-text content. */ function decodeFileContent(content, mimeType) { const decodedBase64 = Buffer.from(content, "base64"); if (decodedBase64.toString("base64") === content) { return decodedBase64; } const isTextContent = mimeType?.startsWith("text/") || mimeType?.includes("csv") || mimeType?.includes("json") || mimeType?.includes("xml"); return isTextContent ? Buffer.from(content, "utf8") : Buffer.from(content, "binary"); } class LocalFileService extends utils_1.AbstractFileProviderService { constructor(_, options) { super(); this.getUploadFilePath = (baseDir, fileKey) => { const resolvedBase = path_1.default.resolve(baseDir); const resolved = path_1.default.resolve(resolvedBase, fileKey); const relative = path_1.default.relative(resolvedBase, resolved); if (relative === ".." || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Invalid file key: ${fileKey}`); } return resolved; }; this.getUploadFileUrl = (fileKey) => { const baseUrl = new URL(this.backendUrl_); baseUrl.pathname = path_1.default.join(baseUrl.pathname, fileKey); return baseUrl.href; }; this.uploadDir_ = options?.upload_dir || path_1.default.join(process.cwd(), "static"); // Since there is no way to serve private files through a static server, we simply place them in `static`. // This means that the files will be available publicly if the filename is known. Since the local file provider // is for development only, this shouldn't be an issue. If you really want to use it in production (and you shouldn't) // You can change the private upload dir to `/private` but none of the functionalities where you use a presigned URL will work. this.privateUploadDir_ = options?.private_upload_dir || path_1.default.join(process.cwd(), "static"); this.backendUrl_ = options?.backend_url || "http://localhost:9000/static"; } async upload(file) { if (!file) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `No file provided`); } if (!file.filename) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `No filename provided`); } const parsedFilename = path_1.default.parse(file.filename); const baseDir = file.access === "public" ? this.uploadDir_ : this.privateUploadDir_; await this.ensureDirExists(baseDir, parsedFilename.dir); const fileKey = path_1.default.join(parsedFilename.dir, // We prepend "private" to the file key so deletions and presigned URLs can know which folder to look into `${file.access === "public" ? "" : "private-"}${Date.now()}-${parsedFilename.base}`); const filePath = this.getUploadFilePath(baseDir, fileKey); const fileUrl = this.getUploadFileUrl(fileKey); const content = decodeFileContent(file.content, file.mimeType); await promises_1.default.writeFile(filePath, content); return { key: fileKey, url: fileUrl, }; } async getUploadStream(fileData) { if (!fileData.filename) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `No filename provided`); } const parsedFilename = path_1.default.parse(fileData.filename); const baseDir = fileData.access === "public" ? this.uploadDir_ : this.privateUploadDir_; await this.ensureDirExists(baseDir, parsedFilename.dir); const fileKey = path_1.default.join(parsedFilename.dir, // We prepend "private" to the file key so deletions and presigned URLs can know which folder to look into `${fileData.access === "public" ? "" : "private-"}${Date.now()}-${parsedFilename.base}`); const filePath = this.getUploadFilePath(baseDir, fileKey); const fileUrl = this.getUploadFileUrl(fileKey); const writeStream = (0, fs_1.createWriteStream)(filePath); const promise = new Promise((resolve, reject) => { writeStream.on("finish", () => { resolve({ url: fileUrl, key: fileKey, }); }); writeStream.on("error", (err) => { reject(err); }); }); return { writeStream, promise, url: fileUrl, fileKey, }; } async delete(files) { files = Array.isArray(files) ? files : [files]; await Promise.all(files.map(async (file) => { const baseDir = file.fileKey.startsWith("private-") ? this.privateUploadDir_ : this.uploadDir_; const filePath = this.getUploadFilePath(baseDir, file.fileKey); try { await promises_1.default.access(filePath, promises_1.default.constants.W_OK); await promises_1.default.unlink(filePath); } catch (e) { // The file does not exist, we don't do anything if (e.code !== "ENOENT") { throw e; } } })); return; } async getDownloadStream(file) { const baseDir = file.fileKey.startsWith("private-") ? this.privateUploadDir_ : this.uploadDir_; const filePath = this.getUploadFilePath(baseDir, file.fileKey); return (0, fs_1.createReadStream)(filePath); } async getAsBuffer(file) { const baseDir = file.fileKey.startsWith("private-") ? this.privateUploadDir_ : this.uploadDir_; const filePath = this.getUploadFilePath(baseDir, file.fileKey); return promises_1.default.readFile(filePath); } // The local file provider doesn't support presigned URLs for private files (i.e files not placed in /static). async getPresignedDownloadUrl(file) { const isPrivate = file.fileKey.startsWith("private-"); const baseDir = isPrivate ? this.privateUploadDir_ : this.uploadDir_; const filePath = this.getUploadFilePath(baseDir, file.fileKey); try { await promises_1.default.access(filePath, promises_1.default.constants.F_OK); } catch { throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `File with key ${file.fileKey} not found`); } return this.getUploadFileUrl(file.fileKey); } /** * Returns the pre-signed URL that the client (frontend) can use to trigger * a file upload. In this case, the Medusa backend will implement the * "/upload" endpoint to perform the file upload. */ async getPresignedUploadUrl(fileData) { if (!fileData?.filename) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `No filename provided`); } return { url: "/admin/uploads", key: fileData.filename, }; } async ensureDirExists(baseDir, dirPath) { const relativePath = path_1.default.join(baseDir, dirPath); try { await promises_1.default.access(relativePath, promises_1.default.constants.F_OK); } catch (e) { await promises_1.default.mkdir(relativePath, { recursive: true }); } return relativePath; } } exports.LocalFileService = LocalFileService; LocalFileService.identifier = "localfs"; //# sourceMappingURL=local-file.js.map