sfs-node
Version:
Core of SFS for node.js
1 lines • 15.7 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/index.ts"],"sourcesContent":["import fs from \"fs\";\r\nimport path from \"path\";\r\nimport checkDiskSpace from \"check-disk-space\";\r\nimport { createHash } from \"crypto\";\r\nimport { v4 as uuidv4 } from \"uuid\";\r\nimport { dotExtensionToCategotry, sfsFileType } from \"sfs-file-type\";\r\n\r\nexport interface UploadedFile {\r\n /** file name */\r\n name: string;\r\n /** A function to move the file elsewhere on your server */\r\n mv(path: string, callback: (err: any) => void): void;\r\n mv(path: string): Promise<void>;\r\n /** Encoding type of the file */\r\n encoding: string;\r\n /** The mimetype of your file */\r\n mimetype: string;\r\n /** A buffer representation of your file, returns empty buffer in case useTempFiles option was set to true. */\r\n data: Buffer;\r\n /** Uploaded size in bytes */\r\n size: number;\r\n /** MD5 checksum of the uploaded file */\r\n md5: string;\r\n}\r\nexport type sfsFileId = string | number;\r\nexport type sfsFile = {\r\n id: sfsFileId;\r\n name: string;\r\n extension: string;\r\n hash: string;\r\n size: number; //Bytes\r\n type: sfsFileType;\r\n last_modified: number; //timestamp,\r\n path: string;\r\n url?: string;\r\n [key: string]: any; // Allow additional dynamic properties\r\n};\r\nexport type loggerLvl = \"info\" | \"success\" | \"error\";\r\n/**\r\n * Configuration for Simple File Storage (SFS)\r\n */\r\nexport type sfsConfig = {\r\n /**\r\n * Folder where files will be stored.\r\n */\r\n publicFolder: string;\r\n\r\n /**\r\n * Base URL prefix to which the file ID will be appended.\r\n */\r\n mask: string;\r\n\r\n /**\r\n * Function that returns an `sfsFile` based on its ID.\r\n * @param id - The file's unique identifier.\r\n * @returns A promise resolving to the corresponding `sfsFile`.\r\n */\r\n getFileById: (id: sfsFileId) => Promise<sfsFile>;\r\n\r\n /**\r\n * Function that returns an `sfsFile` based on its content hash.\r\n * @param hash - The hash of the file contents.\r\n * @returns A promise resolving to the corresponding `sfsFile`.\r\n */\r\n getFileByHash: (hash: string) => Promise<sfsFile>;\r\n\r\n /**\r\n * Function that stores an `sfsFile` and returns the stored object.\r\n * @param file - The `sfsFile` to be created.\r\n * @returns A promise resolving to the stored `sfsFile`.\r\n */\r\n createFile: (file: sfsFile) => Promise<sfsFile>;\r\n\r\n /**\r\n * Optional logger function. Leave undefined to disable logging.\r\n * @param message - Log message or object.\r\n * @param lvl - Optional log level (e.g., info, warn, error).\r\n */\r\n logger?: (message: any, lvl?: loggerLvl) => void;\r\n\r\n /**\r\n * Optional function for generating unique IDs.\r\n * Defaults to `uuidv4()` if not provided.\r\n * @returns A unique string or number.\r\n */\r\n uid?: () => string | number;\r\n\r\n allowDuplicates?: boolean;\r\n cleanupOnFailedUpload?: boolean;\r\n};\r\n\r\n/**\r\n * Initializes core logic functions for the Simple File Storage (SFS) system.\r\n *\r\n * @param config - The configuration object for the SFS system.\r\n * @param config.publicFolder - Path to the folder where uploaded files will be stored.\r\n * @param config.mask - Base URL prefix for constructing public file URLs (e.g., \"https://example.com/files\").\r\n * @param config.getFileById - Async function that retrieves an `sfsFile` by its unique ID.\r\n * @param config.getFileByHash - Async function that retrieves an `sfsFile` by its content hash.\r\n * @param config.createFile - Async function that stores an `sfsFile` and returns the stored object.\r\n * @param config.logger - Optional logger function for internal operations. No logging if undefined.\r\n * @param config.uid - Optional function for generating unique IDs (defaults to `uuidv4` if not provided).\r\n * @param config.allowDuplicates - Flag deterining whether to allow storing duplicate files in database, important for clientside optimistic uploads\r\n *\r\n * @returns An object containing internal logic functions used by the SFS system.\r\n */\r\n\r\nexport default function initFunctions({\r\n publicFolder,\r\n mask,\r\n getFileById,\r\n getFileByHash,\r\n createFile,\r\n logger = undefined,\r\n uid = uuidv4,\r\n allowDuplicates = false,\r\n cleanupOnFailedUpload = false,\r\n}: sfsConfig) {\r\n /**\r\n * Converts a URL to its corresponding file ID by removing the mask prefix.\r\n * Handles cases where the mask may or may not end with a slash.\r\n *\r\n * @param url - The URL string to convert.\r\n * @returns The extracted file ID from the URL.\r\n */\r\n const urlToId = (url: string) =>\r\n mask.endsWith(\"/\") ? url.replace(mask, \"\") : url.replace(mask + \"/\", \"\");\r\n\r\n /**\r\n * Converts a file ID to its corresponding URL by prepending the mask prefix.\r\n * Handles cases where the mask may or may not end with a slash.\r\n *\r\n * @param id - The file ID to convert to a URL.\r\n * @returns The constructed URL string for the given file ID.\r\n */\r\n const idToUrl = (id: sfsFileId) =>\r\n (mask.endsWith(\"/\") ? mask : mask + \"/\") + id;\r\n\r\n /**\r\n * Resolves the absolute file system path and original name of a stored file by its ID.\r\n *\r\n * This function retrieves metadata for the given file ID using `getFileById`,\r\n * and constructs the full path to the file in the `publicFolder`, based on its hash and extension.\r\n *\r\n * @param id - The unique identifier of the stored file.\r\n * @returns An object containing the full file system path (`filePath`) and original file name (`fileName`),\r\n * or `undefined` if the file was not found.\r\n */\r\n const resolveFilePath = async (id: sfsFileId) => {\r\n try {\r\n const fileInfo = await getFileById(id);\r\n const { hash, extension, name } = fileInfo;\r\n const filePath = hash + extension;\r\n return { filePath: path.join(publicFolder, filePath), fileName: name };\r\n } catch (err) {\r\n logger && logger(err, \"error\");\r\n }\r\n };\r\n\r\n /**\r\n * Handles upload and deduplication logic for a single file.\r\n *\r\n * - Calculates SHA-256 hash of the file content in-memory.\r\n * - Determines file extension from name or infers from binary data.\r\n * - Avoids storing duplicates by using `hash + extension` as unique filename.\r\n * - Saves file to disk if not already stored.\r\n * - Registers file metadata via `createFile`, or reuses existing entry.\r\n *\r\n * @param file - Uploaded file object (e.g., from express-fileupload).\r\n * @param options - Optional configuration object.\r\n * @param options.filePath - Logical file path (or folder-relative path) for storing metadata. Defaults to \"/\".\r\n * @param options.id - Custom file ID. If not provided, a unique ID will be generated using `uid()`.\r\n * @param options.additionalFields - Additional metadata to store with the file.\r\n * @returns An `sfsFile` object with generated ID and public URL.\r\n *\r\n * @throws Will throw if file saving or metadata operations fail.\r\n */\r\n\r\n const getFileTypeFromBuffer = async (fileData: Uint8Array | ArrayBuffer) => {\r\n const { fileTypeFromBuffer } = await import(\"file-type\");\r\n return await fileTypeFromBuffer(fileData);\r\n };\r\n\r\n const saveFile = async (\r\n file: UploadedFile,\r\n options?: {\r\n filePath?: string;\r\n id?: sfsFileId;\r\n additionalFields?: any;\r\n }\r\n ) => {\r\n try {\r\n // Set default values\r\n const filePath = options?.filePath || \"/\";\r\n const id = options?.id || uid();\r\n\r\n // Save file\r\n const name = decodeURI(file.name);\r\n const hash = createHash(\"sha256\").update(file.data).digest(\"hex\");\r\n // Get extenison\r\n let extension = \"\";\r\n const extensionFromName = path.extname(name);\r\n if (extensionFromName) {\r\n extension = extensionFromName;\r\n } else {\r\n const filetype = await getFileTypeFromBuffer(file.data);\r\n if (filetype) {\r\n extension = `.${filetype.ext}`;\r\n }\r\n }\r\n // Filename is its hash + extension to avoid storing duplicate files with different names\r\n const constPath = path.join(publicFolder, hash + extension);\r\n let fileInfo = await getFileByHash(hash);\r\n\r\n // File exists\r\n if (fileInfo) {\r\n logger && logger(\"SFS: File already uploaded\", \"info\");\r\n }\r\n // File is new\r\n else {\r\n logger && logger(\"SFS: Saving file\", \"success\");\r\n await file.mv(constPath);\r\n }\r\n // File is new or doesnt exist in this folder or duplocates are allowed\r\n if (!!!fileInfo || filePath !== fileInfo.path || allowDuplicates) {\r\n const size = fileInfo?.size || fs.statSync(constPath).size;\r\n const type = fileInfo?.type || dotExtensionToCategotry(extension);\r\n const now = Date.now();\r\n const fileData = {\r\n id,\r\n name,\r\n extension,\r\n hash,\r\n size,\r\n type,\r\n last_modified: now,\r\n path: filePath,\r\n publishedAt: now,\r\n ...options?.additionalFields,\r\n };\r\n\r\n try {\r\n const mutationResult = await createFile(fileData);\r\n mutationResult.url = idToUrl(mutationResult.id);\r\n return mutationResult;\r\n } catch (createError) {\r\n // Cleanup: Remove the physical file if database operation failed\r\n if (cleanupOnFailedUpload && !fileInfo) {\r\n fs.unlinkSync(constPath);\r\n logger &&\r\n logger(\r\n \"SFS: Cleaning up orphaned file after database error\",\r\n \"info\"\r\n );\r\n }\r\n }\r\n }\r\n\r\n // File exists in this folder\r\n else {\r\n logger && logger(\"File already exists at this location\", \"error\");\r\n fileInfo.url = idToUrl(fileInfo.id);\r\n\r\n return fileInfo;\r\n }\r\n } catch (err) {\r\n logger && logger(\"Upload error\", \"error\");\r\n throw new Error(err);\r\n }\r\n };\r\n const deleteFileByHash = async (hash: string) => {\r\n try {\r\n const files = fs.readdirSync(publicFolder);\r\n const fileToDelete = files.find((f) => f.split(\".\")[0] === hash);\r\n if (!fileToDelete) {\r\n throw new Error(`File with hash ${hash} not found in ${publicFolder}`);\r\n }\r\n const pathToFile = path.join(publicFolder, fileToDelete);\r\n fs.unlinkSync(pathToFile);\r\n } catch (err) {\r\n throw new Error(err);\r\n }\r\n };\r\n const deleteFileById = async (id: string) => {\r\n try {\r\n const { filePath } = await resolveFilePath(id);\r\n fs.unlinkSync(filePath);\r\n } catch (err) {\r\n throw new Error(err);\r\n }\r\n };\r\n const getDiskUsage = async (req, res) => {\r\n const diskSpace = await checkDiskSpace(publicFolder);\r\n return diskSpace;\r\n };\r\n return {\r\n resolveFilePath,\r\n idToUrl,\r\n urlToId,\r\n saveFile,\r\n deleteFileByHash,\r\n deleteFileById,\r\n getDiskUsage,\r\n };\r\n}\r\n"],"names":["_ref","publicFolder","mask","getFileById","getFileByHash","createFile","_ref$logger","logger","undefined","_ref$uid","uid","uuidv4","v4","_ref$allowDuplicates","allowDuplicates","_ref$cleanupOnFailedU","cleanupOnFailedUpload","idToUrl","id","endsWith","resolveFilePath","Promise","resolve","_catch","then","fileInfo","name","filePath","path","join","hash","extension","fileName","err","e","reject","urlToId","url","replace","saveFile","file","options","_temp4","constPath","_temp2","size","fs","statSync","type","dotExtensionToCategotry","now","Date","fileData","_extends","last_modified","publishedAt","additionalFields","mutationResult","unlinkSync","_temp","mv","decodeURI","createHash","update","data","digest","extensionFromName","extname","_temp3","_ref2","fileTypeFromBuffer","getFileTypeFromBuffer","filetype","ext","Error","deleteFileByHash","fileToDelete","readdirSync","find","f","split","pathToFile","deleteFileById","_ref3","getDiskUsage","req","res","checkDiskSpace"],"mappings":"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"}