UNPKG

@trap_stevo/filetide

Version:

Revolutionizing real-time file transfer with seamless, instant communication across any device. Deliver files instantly, regardless of platform, and experience unparalleled speed and control in managing transfers. Elevate your file-sharing capabilities wi

484 lines (468 loc) 19.1 kB
"use strict"; const path = require("path"); const fsAsync = require("fs").promises; const fs = require("fs"); const os = require("os"); const filesPath = path.join(__dirname, "../files"); class FileUtilityManager { /** * Recursively calculates the total size of files in a directory. * @param {string} dirPath - The directory path. * @returns {number} - The total size in bytes of the directory's contents. */ calculateDirectorySize(dirPath) { let totalSize = 0; const files = fs.readdirSync(dirPath); files.forEach(file => { const filePath = path.join(dirPath, file); const stats = fs.statSync(filePath); if (stats.isDirectory()) { totalSize += this.calculateDirectorySize(filePath); } else { totalSize += stats.size; } }); return totalSize; } /** * Function to filter files and directories. * @param {string} dirPath - The directory path to scan. * @param {Array} filterContent - Array of paths (files, directories, extensions) to avoid (e.g., ['docs', 'example.txt', '.js']). * @returns {Promise<Array>} - A promise that resolves to an array of objects containing details about filtered files and directories. */ static async filterFilesAndDirectories(dirPath, filterContent = []) { try { const currentDirPath = this.getTidePath(dirPath); const items = await fs.promises.readdir(currentDirPath, { withFileTypes: true }); const results = []; for (const item of items) { const itemPath = path.join(currentDirPath, item.name); if (item.name.startsWith(".")) { continue; } const filtered = filterContent.some(filterEntry => { const resolvedFilterEntry = path.resolve(currentDirPath, filterEntry); const extensionFilter = filterEntry.startsWith("."); return itemPath === resolvedFilterEntry || item.name === filterEntry || extensionFilter && path.extname(item.name) === filterEntry; }); if (!filtered) { const stats = await fs.promises.stat(itemPath); results.push({ type: item.isDirectory() ? "directory" : "file", dateCreated: stats.birthtime, size: stats.size, name: item.name, path: itemPath }); } } return results; } catch (error) { console.log(`Did not filter contents from ${dirPath} ~ `, error); throw error; } } /** * Function to list all paths across the system using wildcard '*'. * @param {number} depth - The maximum depth limit (from the root). Defaults to 1. * @param {Function} onDirectory - Callback that outputs each directory's contents on the go. * @returns {Promise<Array>} - A promise that resolves to an array of file and directory metadata across the system. */ static async listAllPathsInOS(depth = 1, onDirectory, onFile) { const rootPaths = ["/", "C:/"]; let allResults = []; const rootPromises = rootPaths.map(async rootPath => { if (await this.exists(rootPath)) { const subResult = await this.listPaths(rootPath, depth, 0, onDirectory); if (depth <= 3) { allResults.push(...subResult.results); } } }); await Promise.all(rootPromises); return allResults; } /** * Function to list all paths and their metadata recursively. * @param {string} dir - The directory path to scan. * @param {number} maxDepth - The maximum depth limit (from the root). Defaults to 1. * @param {number} currentDepth - The current depth level. Starts at 0 (for the root). * @param {Function} onDirectory - Callback that outputs each directory's contents on the go. * @returns {Promise<object>} - A promise that resolves to an object containing a list of files/directories with metadata. */ static async listPaths(dir, maxDepth = 1, currentDepth = 0, onDirectory, onContent) { let results = []; if (currentDepth > maxDepth) { return { totalItems: 0, totalSize: 0, results }; } try { const list = await fsAsync.readdir(dir, { withFileTypes: true }); const dirStats = await fsAsync.stat(dir); const dirMetadata = { totalItems: 0, totalSize: 0, createdTime: dirStats.birthtime, lastModified: dirStats.mtime, type: "directory", path: dir }; results.push(dirMetadata); const processEntries = list.map(async file => { const filePath = FileUtilityManager.getTidePath(path.join(dir, file.name)); try { if (file.isDirectory()) { console.log(`[FileTide] Depth >> ${currentDepth} | Including directory ~ ${filePath}`); const subDirResult = await this.listPaths(filePath, maxDepth, currentDepth + 1, onDirectory); dirMetadata.totalItems += subDirResult.totalItems; dirMetadata.totalSize += subDirResult.totalSize; results.push(...subDirResult.results); if (onDirectory) { onDirectory(subDirResult.totalItems, subDirResult.results); } } else { console.log(`[FileTide] Depth >> ${currentDepth} | Including file ~ ${filePath}`); const fileStats = await fsAsync.stat(filePath); const fileMetadata = { createdTime: fileStats.birthtime, lastModified: fileStats.mtime, size: fileStats.size, path: filePath, type: "file" }; dirMetadata.totalItems += 1; dirMetadata.totalSize += fileStats.size; results.push(fileMetadata); } } catch (error) { if (error.code === "ENOENT") { console.log(`[FileTide] Warning ~ File or directory not found - ${filePath}`); } else { console.log(`[FileTide] Error processing path ~ ${filePath} ~ `, error); } } }); const concurrencyLimit = 5; for (let i = 0; i < processEntries.length; i += concurrencyLimit) { await Promise.all(processEntries.slice(i, i + concurrencyLimit)); } return { totalItems: dirMetadata.totalItems, totalSize: dirMetadata.totalSize, results }; } catch (error) { if (error.code === 'ENOENT') { console.log(`[FileTide] Warning ~ Directory not found - ${dir}`); } else { console.log(`[FileTide] Error processing directory ~ ${dir} ~ `, error); } return { totalItems: 0, totalSize: 0, results }; } } /** * Allows for listing files in a specific directory or all files in the system '*'. * @param {Array} directories - Array of directories to scan or '*' for entire system. * @param {number} depth - The maximum depth limit (from the root). Defaults to 1. * @param {Funtion} onDirectory - Callback that outputs each directory's contents on the go. * @returns {Promise<Array>} - A promise that resolves to an array of file and directory metadata from specified directories or the entire system. */ static async listFiles(directories, depth = 1, onDirectory) { let fileList = []; if (directories.includes("*")) { fileList = await this.listAllPathsInOS(depth, onDirectory); } else { const dirPromises = directories.map(async dir => { const currentDir = this.getTidePath(dir); if (await this.exists(currentDir)) { const subResult = await this.listPaths(currentDir, depth, 0, onDirectory); if (depth <= 3) { fileList.push(...subResult.results); } } else { console.warn(`Directory does not exist ~ ${currentDir}`); } }); await Promise.all(dirPromises); } return fileList; } /** * Checks the path's type. * @param {string} inputPath - The path to check. * @returns {Promise<boolean>} - Returns a promise that resolves to true if a directory, false otherwise. */ static async directoryPath(inputPath) { const stats = await fsAsync.stat(inputPath); return stats.isDirectory(); } /** * Determines the type of a given path (directory or file) based on conventions. * @param {string} inputPath - The path to infer. * @returns {string} - Returns "directory" if the path has no extension, "file" if it contains an extension. */ static inferPathType(inputPath) { const parsedPath = path.parse(inputPath); if (parsedPath.ext) { return "file"; } return "directory"; } /** * Helper function to check if a path exists. * @param {string} inputPath - The path to check. * @returns {Promise<boolean>} - Returns a promise that resolves to true if the path exists, false otherwise. */ static async exists(inputPath) { try { await fsAsync.access(inputPath); return true; } catch (e) { return false; } } /** * Function to check for a path or any of the path's subpaths inside the user's open shore (allowed drop-zone) policy. * Supports a wildcard '*' which allows all paths. * @param {string} shorePath - The file or directory path to check * @param {Array} openShores - Array of paths or '*' to check against * @returns {boolean} - Returns true if openShores found the itemPath or the itemPath's subpath. */ static allowedShore(shorePath, openShores = []) { const resolvedShorePath = path.resolve(shorePath); if (openShores.includes("*")) { return true; } return openShores.some(listedShore => { const currentShore = path.resolve(listedShore); return resolvedShorePath === currentShore || resolvedShorePath.startsWith(currentShore + path.sep); }); } static getTidePath(inputPath) { let homeDir = os.homedir(); const platform = process.platform; let currentUserDir = homeDir; if (platform === "linux") { const username = process.env.USER || process.env.LOGNAME; homeDir = "/home"; currentUserDir = path.join(homeDir, username); } const folderMappings = { ">Downloads": path.join(homeDir, "Downloads"), ">Documents": path.join(homeDir, "Documents"), ">Desktop": path.join(homeDir, "Desktop"), ">AppData": platform === "win32" ? path.join(homeDir, "AppData", "Roaming") : path.join(homeDir, ".config"), ">LocalAppData": platform === "win32" ? path.join(homeDir, "AppData", "Local") : path.join(homeDir, ".local", "share"), ">Music": path.join(homeDir, "Music"), ">Photos": platform === "win32" ? path.join(homeDir, "Pictures") : path.join(homeDir, "Photos"), ">Videos": path.join(homeDir, "Videos"), ">Public": platform === "win32" ? path.join("C:", "Users", "Public") : "/usr/share", ">Templates": path.join(homeDir, "Templates"), ">Temp": platform === "win32" ? path.join("C:", "Windows", "Temp") : "/tmp", ">LogFiles": platform === "win32" ? path.join("C:", "Windows", "Logs") : "/var/log", ">SystemRoot": platform === "win32" ? path.join("C:", "Windows") : "/", ">Cache": platform === "win32" ? path.join(homeDir, "AppData", "Local", "Cache") : path.join(homeDir, ".cache"), ">Config": platform === "win32" ? path.join(homeDir, "AppData", "Local", "Config") : "/etc", ">CurrentDirectory": process.cwd(), ">CurrentUser": currentUserDir, ">Home": homeDir }; let resolvedPath = inputPath; Object.keys(folderMappings).forEach(shortcut => { if (resolvedPath.includes(shortcut)) { const replacementPath = folderMappings[shortcut]; if (fs.existsSync(replacementPath)) { resolvedPath = resolvedPath.replace(shortcut, replacementPath); } else { console.log(`The directory for '${shortcut}' does not exist on this system.`); resolvedPath = resolvedPath.replace(shortcut, homeDir); } } }); resolvedPath = this.osPath(resolvedPath); resolvedPath = this.normalizePath(resolvedPath); return resolvedPath; } /** * Normalizes the input path to handle both single and double slashes. * @param {string} inputPath - The path to normalize. * @returns {string} - A normalized path with proper slashes. */ static normalizePath(inputPath) { return path.normalize(inputPath); } static osPath(filePath) { let resolvedPath = this.normalizePath(filePath); resolvedPath = resolvedPath.replace(/\\/g, "/"); resolvedPath = resolvedPath.replace(/\/{2,}/g, "/"); if (resolvedPath.startsWith("//")) { return resolvedPath; } return resolvedPath; } /** * Gets all files' data from a given directory. * @param {string} dirPath - The path to the directory. * @param {Array} filterContent - The paths to filter from the directory. * @returns {Array} - An array of objects containing file data, name, and path. */ static async getDirectoryData(inputDirPath, filterContent = []) { const dirPath = this.getTidePath(inputDirPath); if (!fs.existsSync(dirPath)) { console.log(`Directory at path ${dirPath} not found.`); return []; } const largeFileThreshold = 2 * 1024 * 1024 * 1024; const filesData = []; async function readDirectory(currentPath) { const files = await FileUtilityManager.filterFilesAndDirectories(currentPath, filterContent); for (const file of files) { const stats = await fs.promises.stat(file.path); if (stats.isFile()) { if (stats.size > largeFileThreshold) { const currentFilePath = FileUtilityManager.getTidePath(file.path); console.log(`Detected large file at ${currentFilePath}, size ~ ${stats.size} bytes.`); filesData.push({ fileName: path.basename(currentFilePath), filePath: currentFilePath, fileSize: stats.size, largeFile: true, type: "file" }); } else if (stats.size <= largeFileThreshold) { const fileData = await FileUtilityManager.getFileData(file.path); if (fileData) { filesData.push(fileData); } } } else if (stats.isDirectory()) { await readDirectory(file.path); } } } await readDirectory(dirPath); return filesData; } /** * Gets file data from a given file path. * @param {string} inputFilePath - The full path to the file. * @returns {Object|null} - An object containing file data, name, path, and size or null if the file doesn't exist. */ static async getFileData(inputFilePath) { const filePath = this.getTidePath(inputFilePath); if (!fs.existsSync(filePath)) { console.log(`File at path ${filePath} not found.`); return null; } const fileName = path.basename(filePath); return new Promise((resolve, reject) => { const fileStream = fs.createReadStream(filePath); const chunks = []; fileStream.on("data", chunk => { chunks.push(chunk); }); fileStream.on("end", () => { const fileData = Buffer.concat(chunks); resolve({ size: fileData.length, fileName, fileData, filePath }); }); fileStream.on("error", error => { console.error(`Did not read file ${filePath}: `, error); resolve(null); }); }); } /** * Gets data from a file or directory based on the provided path. * @param {string} inputPath - The path to a file or directory. * @param {Array} filterContent - The paths to filter from the directory if getting a directory. * @returns {Object|Array|null} - File data if it's a file, an array of file data if it's a directory, or null if the path doesn't exist. */ static async getPathData(inputPath, filterContent = []) { const currentInputPath = this.getTidePath(inputPath); if (!fs.existsSync(currentInputPath)) { console.log(`Path at ${currentInputPath} not found.`); return null; } const largeFileThreshold = 2 * 1024 * 1024 * 1024; const stats = fs.statSync(currentInputPath); if (stats.isFile()) { if (stats.size > largeFileThreshold) { console.log(`Detected large file at ${currentInputPath}, size ~ ${stats.size} bytes.`); return { fileName: path.basename(currentInputPath), filePath: currentInputPath, fileSize: stats.size, largeFile: true, type: "file" }; } return { content: await this.getFileData(currentInputPath), largeFile: false, type: "file" }; } else if (stats.isDirectory()) { return { content: await this.getDirectoryData(currentInputPath, filterContent), type: "directory" }; } console.log(`Path at ${currentInputPath} neither a file nor a directory.`); return null; } /** * Save a chunk of a file for a specific user to the specified path with a custom file name. * @param {String} userID - The ID of the user for whom the file is being saved. * @param {Buffer} chunk - The file chunk to save. * @param {String} savePath - The directory where the file should be saved. * @param {String} fileName - The name of the file being saved. */ static saveChunk(userID, chunk, savePath, fileName = "${Date.now()}_temp_file") { const filePath = path.join(savePath || filesPath, fileName || `${userID}_${Date.now()}`); fs.appendFileSync(filePath, chunk); console.log(`Saved chunk for user ${userID} to ${filePath}.`); } /** * Assemble the final file from chunks for a specific user with a custom file name. * @param {String} userID - The ID of the user whose file is being assembled. * @param {String} savePath - The directory where the file should be saved. * @param {String} fileName - The name of the final file to assemble. */ static assembleFile(userID, savePath, fileName = `${Date.now()}_final_file`) { const tempFilePath = path.join(savePath || filesPath, `${userID}_${Date.now()}`); const finalFilePath = path.join(savePath || filesPath, fileName || `${userID}_${Date.now()}`); fs.renameSync(tempFilePath, finalFilePath); console.log(`Assembled file for user ${userID} at ${finalFilePath}.`); } /** * Save a complete file directly to a specified path with a custom file name. * @param {String} fileName - The name of the file to save. * @param {Buffer} fileData - The complete file data to save. * @param {String} savePath - The directory where the file should be saved. */ static saveFile(fileName = "${Date.now()}_temp_file", fileData, savePath) { const filePath = path.join(savePath || filesPath, fileName || `${userID}_${Date.now()}`); fs.writeFileSync(filePath, fileData); console.log(`Saved file ${fileName} to ${filePath}.`); } } module.exports = { FileUtilityManager, filesPath };