@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
JavaScript
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
};
;