@medusajs/file-local
Version:
Local filesystem file storage for Medusa
189 lines • 8.77 kB
JavaScript
;
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