file/file_basic.js

//Initialise functions
{
	if (!global.File)
		/**
		 * The namespace for all UF/File utility functions, typically for static methods.
		 * 
		 * @namespace File
		 */
		global.File = {};
	
	/**
	 * Whether a file is inside a folder path.
	 * @alias File.containsPath
	 * 
	 * @param {string} arg0_file_path
	 * @param {string} arg1_folder_path
	 * 
	 * @returns {boolean}
	 */
	//[QUARANTINE]
	File.containsPath = function (arg0_file_path, arg1_folder_path) {
		const resolvedFile = path.resolve(arg0_file_path);
		const resolvedFolder = path.resolve(arg1_folder_path);
		
		//Return statement
		return (
			resolvedFile === resolvedFolder ||
			resolvedFile.startsWith(resolvedFolder + path.sep)
		);
	};
	
	/**
	 * Converts an image, preferably `.png`, to a Base64 string for inline embedding.
	 * @alias File.convertImageToBase64
	 * 
	 * @param {string} arg0_file_path
	 * 
	 * @returns {string}
	 */
	File.convertImageToBase64 = function (arg0_file_path) {
		//Convert from parameters
		let file_buffer = fs.readFileSync(arg0_file_path);
		
		//Return statement
		return file_buffer.toString("base64");
	};
	
	/**
	 * Returns all drives in the current operating system.
	 * @alias File.getAllDrives
	 * 
	 * @returns {string[]}
	 */
	//[QUARANTINE]
	File.getAllDrives = function () {
		const platform = process.platform;
		
		try {
			if (platform === "win32") {
				// Run WMIC command to list logical drives
				const output = child_process.execSync("wmic logicaldisk get name", { encoding: "utf8" });
				return output
				.split("\n")
				.map((line) => line.trim())
				.filter((line) => /^[A-Z]:$/i.test(line)) // e.g. "C:"
				.map((drive) => drive + "\\");
			} else {
				// POSIX systems
				// Common mount points: "/", "/Volumes/*" (macOS), "/mnt/*" or "/media/*" (Linux)
				const drives = ["/"];
				
				const possibleDirs = ["/Volumes", "/mnt", "/media"];
				for (const dir of possibleDirs) {
					if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
						const subMounts = fs
						.readdirSync(dir)
						.map((name) => path.join(dir, name))
						.filter((p) => {
							try {
								return fs.statSync(p).isDirectory();
							} catch {
								return false;
							}
						});
						drives.push(...subMounts);
					}
				}
				return drives;
			}
		} catch (err) {
			console.error("getAllDrives() failed:", err);
			return [];
		}
	};
	
	/**
	 * Returns all files in a current folder path as full file paths.
	 * @alias File.getAllFiles
	 * 
	 * @param {string} arg0_folder_path
	 * @param {Object} [arg1_options]
	 *  @param {string[]} [arg1_options.excluded_paths]
	 * 
	 * @returns {string[]}
	 */
	File.getAllFiles = async function (arg0_folder_path, arg1_options) {
		//Convert from parameters
		let root_path = path.resolve(arg0_folder_path);
		let options = arg1_options || {};
		
		//Declare local instance variables
		let excluded_paths = (options.excluded_paths || []).map((p) => path.resolve(p));
		
		try {
			let entries = await fs.promises.readdir(root_path, { withFileTypes: true });
			let paths = await Promise.all(
				entries.map(async (entry) => {
					let full_path = path.resolve(root_path, entry.name);
					
					//Internal guard clause if the current full path is in the excluded list
					if (excluded_paths.includes(full_path))
						return [];
					if (entry.isDirectory())
						//Recursively call File.getAllFiles if possible
						return await File.getAllFiles(full_path, options);
					return full_path;
				}),
			);
			
			return paths.flat();
		} catch (err) {
			console.error(`File.getAllFiles: Error reading directory ${root_path}:`, err);
			throw err;
		}
	};
	
	/**
	 * Returns all files in a current folder synchronously.
	 * @alias File.getAllFilesSync
	 * 
	 * @param {string} arg0_folder_path
	 * 
	 * @returns {string[]}
	 */
	File.getAllFilesSync = function (arg0_folder_path) {
		//Convert from parameters
		let folder_path = arg0_folder_path;
		
		//Declare local instance variables
		let all_files = fs.readdirSync(folder_path, {
			recursive: true,
			withFileTypes: true
		});
		
		//Return statement
		return all_files.filter((entry) => entry.isFile())
			.map((entry) => path.join(entry.path, entry.name));
	};
	
	/**
	 * Returns the time at which a file was last modified.
	 * @alias File.getLastModified
	 * 
	 * @param {string} arg0_file_path
	 * 
	 * @returns {Promise<{last_modified: Date, seconds: number}>}
	 */
	File.getLastModified = function (arg0_file_path) {
		//Convert from parameters
		let file_path = arg0_file_path;
		
		//Check if file_path exists, then extract last modified time
		if (fs.existsSync(file_path)) {
			//Declare local instance variables
			let date_obj = new Date();
			let stats = fs.statSync(file_path);
			
			let last_modified = stats.mtime;
			
			let diff_in_seconds = Math.floor((date_obj.getTime() - last_modified.getTime())/1000);
			
			//Return statement
			return {
				last_modified: last_modified,
				seconds: diff_in_seconds
			};
		}
	};
	
	/**
	 * Returns whether the given file path is a file.
	 * @alias File.isFile
	 * 
	 * @param {string} arg0_file_path
	 * 
	 * @returns {boolean}
	 */
	File.isFile = function (arg0_file_path) { return (!File.isFolder(arg0_file_path)); }
	
	/**
	 * Returns whether the given file path is a folder.
	 * @alias File.isFolder
	 * 
	 * @param {string} arg0_file_path
	 * 
	 * @returns {boolean}
	 */
	File.isFolder = function (arg0_file_path) {
		//Convert from parameters
		let file_path = path.resolve(arg0_file_path);
		
		//Return statement
		if (fs.existsSync(file_path))
			return (fs.statSync(file_path).isDirectory());
		return false;
	};
	
	/**
	 * Whether the selected file path is a valid drive.
	 * @alias File.isDrive
	 * 
	 * @param {string} arg0_file_path
	 * 
	 * @returns {boolean}
	 */
	File.isDrive = function (arg0_file_path) {
		//Convert from parameters
		let file_path = arg0_file_path;
		
		//Declare local instance variables
		let resolved = path.resolve(file_path);
		
		//Check if file_path is drive
		try {
			let stats = fs.statSync(resolved);
			if (!stats.isDirectory()) return false; //Internal guard clause if not a directory
		} catch (e) {
			//Return statement
			return false;
		}
		
		//Return statement
		return (resolved === path.parse(resolved).root);
	};
	
	//[QUARANTINE]
	File.loadCSVAsJSON = function (arg0_file_path, arg1_options) {
		//Convert from parameters
		let file_path = arg0_file_path;
		let options = arg1_options ? arg1_options : {};
		
		//Intialise options
		if (!options.mode) options.mode = "vertical";
		
		//Declare local instance variables
		let csv_string = fs.readFileSync(file_path, "utf8");
		let csv_array = csv_string.trim().split(/\r?\n/);
		let parsed_rows = csv_array.map(parseCSVLine);
		let return_obj = {};
		
		if (options.mode === "vertical") {
			let headers = parsed_rows[0];
			for (let i = 1; i < parsed_rows.length; i++) {
				let row = parsed_rows[i];
				let key = row[0];
				if (!key) continue;
				if (!return_obj[key]) {
					return_obj[key] = {};
					for (let j = 1; j < headers.length; j++) {
						return_obj[key][headers[j]] = [];
					}
				}
				for (let j = 1; j < headers.length; j++) {
					return_obj[key][headers[j]].push(row[j] !== undefined ? row[j] : null);
				}
			}
		} else if (options.mode === "horizontal") {
			// In horizontal mode, each column after the first is a key
			let property_names = parsed_rows[0];
			for (let col = 1; col < property_names.length; col++) {
				var key = property_names[col];
				if (!key) continue;
				if (!return_obj[key]) {
					return_obj[key] = {};
					// Initialize arrays for each row label (excluding the first row)
					for (let row = 1; row < parsed_rows.length; row++) {
						var row_label = parsed_rows[row][0];
						return_obj[key][row_label] = [];
					}
				}
				for (let row = 1; row < parsed_rows.length; row++) {
					var row_label = parsed_rows[row][0];
					var value = parsed_rows[row][col] !== undefined ? parsed_rows[row][col] : null;
					return_obj[key][row_label].push(value);
				}
			}
		}
		
		function parseCSVLine (line) {
			let current = "";
			let in_quotes = false;
			let result = [];
			for (let i = 0; i < line.length; i++) {
				if (line[i] === '"' && (i === 0 || line[i - 1] !== "\\")) {
					in_quotes = !in_quotes;
				} else if (line[i] === "," && !in_quotes) {
					result.push(current.trim().replace(/^"|"$/g, "").replace(/""/g, '"'));
					current = "";
				} else {
					current += line[i];
				}
			}
			result.push(current.trim().replace(/^"|"$/g, "").replace(/""/g, '"'));
			return result;
		}
		
		return return_obj;
	};
}