vercengen/components/ComponentFileExplorer/file_operations_ui.js

global.stream_promises = require("stream/promises");

//Initialise functions
{
	/**
	 * Copies specific file paths in `arg0_file_paths` to `arg1_file_path`.
	 * - Method of: {@link ve.FileExplorer}
	 *
	 * @alias #copy
	 * @memberof ve.Component.ve.FileExplorer
	 * 
	 * @param {string[]} arg0_file_paths - The file paths to copy.
	 * @param {string} arg1_file_path - Where to copy the file paths to.
	 * @param {function} arg2_function - Callback function once the copy operation is finished.
	 */
	ve.FileExplorer.copy = function (arg0_file_paths, arg1_file_path, arg2_function) {
		//Convert from parameters
		let file_paths = arg0_file_paths;
		let file_path = arg1_file_path;
		let callback_function = arg2_function;
		
		//Declare local instance variables
		let currently_resolved = false;
		let dialog_window = new ve.Window({
			html: new ve.HTML([
				`<progress id = "files-progress" value = "0" max = "100"></progress><label for = "files-progress"></label>`,
				`<progress id = "file-progress" value = "0" max = "100"></progress><label for = "file-progress"></label>`,
				`<div id = "current-file"></div>`
			].join("<br>")),
			confirmation_prompt: new ve.RawInterface({
				//Limit: currently_resolved === false
				overwrite_button: new ve.Button((e) => {
					overwrite = true;
				}, { name: loc("ve.registry.localisation.FileExplorer_overwrite") }),
				skip_button: new ve.Button((e) => {
					skip = true;
				}, { name: loc("ve.registry.localisation.FileExplorer_skip") }),
				overwrite_all_button: new ve.Button((e) => {
					overwrite_all = true;
				}, { name: loc("ve.registry.localisation.FileExplorer_overwrite_all") })
			}, { name: " ", limit: () => (currently_resolved === false), style: { display: "flex" } })
		}, { 
			attributes: { class: "file-explorer-modal" }, 
			name: loc("ve.registry.localisation.FileExplorer_copying_files_to", String.formatNumber(file_paths.length), file_path), 
			width: "24rem" 
		});
		let html_el = dialog_window.components_obj.html.element;
		let overwrite_all = false;
		let overwrite = false;
		let skip = false;
		
		function copyFileAtIndex (arg0_file_path) {
			//Convert from parameters
			let local_file_path = arg0_file_path;
			if (local_file_path === undefined) {
				if (callback_function !== undefined) callback_function(dialog_window);
				return;
			}
			
			//Declare local instance variables
			currently_resolved = false; //Call new check
			overwrite = false;
			skip = false;
			
			try {
				let file_index = file_paths.indexOf(local_file_path);
				let target_name = path.basename(local_file_path);
				let target_path =  fs.statSync(file_path).isDirectory() ?
					path.join(file_path, target_name) : file_path;
				
				//Update files-progress based on file_index
				let files_progress_bar_el = html_el.querySelector(`progress#files-progress`);
				let files_progress_label_el = html_el.querySelector(`label[for="files-progress"]`);
				
				files_progress_bar_el.value = ((file_index + 1) / file_paths.length)*100;
				files_progress_label_el.innerHTML = loc("ve.registry.FileExplorer_of_progress", String.formatNumber(file_index + 1), String.formatNumber(file_paths.length));
				
				//Polling check to make sure files meet conditions
				if (fs.existsSync(target_path) && overwrite_all === false) {
					let resolution_loop = setInterval(async () => {
						if (currently_resolved) return;
						
						//Listen for a resolution as often as possible
						if (overwrite || overwrite_all) {
							currently_resolved = true;
							fs.unlink(target_path, () => {
								copyFileWithProgress(local_file_path, target_path).then(() => {
									copyNextFile(file_index + 1);
								});
							})
						} else if (skip) {
							currently_resolved = true;
							copyNextFile(file_index + 1);
						}
						
						//KEEP AT BOTTOM! Resolves resolution_loop
						if (currently_resolved === true)
							clearInterval(resolution_loop);
					}, 100);
				} else {
					currently_resolved = true;
					copyFileWithProgress(local_file_path, target_path).then(() => {
						copyNextFile(file_index + 1);
					});
				}
			} catch (e) {
				let local_error_div = document.createElement("div");
					local_error_div.innerHTML = `<div class = "error">${loc("ve.registry.localisation.FileExplorer_error", e.stack)}</div>`;
				dialog_window.html.element.appendChild(local_error_div);
			}
			
			function copyNextFile (arg0_next_index) {
				//Convert from parameters
				let next_index = arg0_next_index;
				
				//Copy file at index
				setTimeout(() => copyFileAtIndex(file_paths[next_index]), 0);
			}
		}
		
		async function copyFileWithProgress (arg0_file_path, arg1_file_path) {
			//Convert from praameters
			let file_path = arg0_file_path;
			let ot_file_path = arg1_file_path;
			
			//Declare local instance variables
			let stats = fs.statSync(file_path);
			
			//Directory case handling
			if (stats.isDirectory()) { //[WIP] - Refactor later
				// Ensure destination directory exists
				if (!fs.existsSync(ot_file_path)) {
					await fs.promises.mkdir(ot_file_path, { recursive: true });
				}
				
				// Recursively copy subentries
				const entries = await fs.promises.readdir(file_path, { withFileTypes: true });
				for (const entry of entries) {
					const srcEntry = path.join(file_path, entry.name);
					const destEntry = path.join(ot_file_path, entry.name);
					await copyFileWithProgress(srcEntry, destEntry); // recursion
				}
				
				// When a folder finishes, render 100% for file progress bar (optional)
				renderFileProgress(1, 0);
				return;
			}
			
			//File case handling
			let copied_bytes = 0;
			let html_el = dialog_window.components_obj.html.element;
			let start_time = Date.now();
			let total_bytes = stats.size;
			
			let destination_stream = fs.createWriteStream(ot_file_path);
			let source_stream = fs.createReadStream(file_path);
			
			html_el.querySelector(`#current-file`).innerHTML = loc("ve.registry.localisation.FileExplorer_copying_to", file_path, ot_file_path);
			source_stream.on("data", (local_chunk) => {
				copied_bytes += local_chunk.length;
				
				let elapsed_time = (Date.now() - start_time)/1000;	
				let percent_progress = copied_bytes/total_bytes;
				let speed = copied_bytes/(elapsed_time || 1); //bytes_per_second
				
				let eta = (total_bytes - copied_bytes)/(speed || 1);
				renderFileProgress(percent_progress, eta);
			});
			await stream_promises.pipeline(source_stream, destination_stream).then(() => {
				renderFileProgress(1, 0);
			});
		}
		
		function renderFileProgress (arg0_percent_progress, arg1_eta_seconds) {
			//Convert from parameters
			let percent_progress = arg0_percent_progress;
			let eta_seconds = arg1_eta_seconds;
			
			//Declare local instance variables
			let file_progress_bar_el = html_el.querySelector(`progress#file-progress`);
			let file_progress_label_el = html_el.querySelector(`label[for="file-progress"]`);
			
			file_progress_bar_el.value = percent_progress*100;
			file_progress_label_el.innerHTML = (eta_seconds > 0) ? 
				loc("ve.registry.localisation.FileExplorer_eta_remaining", Math.round(file_progress_bar_el.value), String.formatDateLength(Math.ceil(eta_seconds))) :
				loc("ve.registry.localisation.FileExplorer_done");
		}
		
		//Begin copy process
		copyFileAtIndex(file_paths[0]);
	};
	
	/**
	 * Deletes selected file paths in `arg0_file_paths` recursively with error logging.
	 * - Method of: {@link ve.FileExplorer}
	 *
	 * @alias #delete
	 * @memberof ve.Component.ve.FileExplorer
	 * 
	 * @param {string[]} arg0_file_paths - The file paths to delete.
	 * @param {function} arg1_function - Callback function once the delete operation is finished.
	 */
	ve.FileExplorer.delete = function (arg0_file_paths, arg1_function) {
		// Convert parameters
		let file_paths = arg0_file_paths;
		let callback_function = arg1_function;
		
		// Create Window with simple progress UI
		let dialog_window = new ve.Window(
			{
				html: new ve.HTML(
					[
						`<progress id="files-progress" value="0" max="100"></progress><label for="files-progress"></label>`,
						`<progress id="file-progress" value="0" max="100"></progress><label for="file-progress"></label>`,
						`<div id="current-file"></div>`,
					].join("<br>")
				),
			},
			{ 
				attributes: { class: "file-explorer-modal" }, 
				name: loc("ve.registry.localisation.FileExplorer_deleting_files", String.formatNumber(file_paths.length)), 
				width: "24rem" 
			}
		);
		
		const html_el = dialog_window.components_obj.html.element;
		
		//
		// Begin deletion process
		//
		deleteFileAtIndex(file_paths[0]);
		
		// ---------------------------------------------------------
		// Helper: iterate through each file or folder sequentially
		// ---------------------------------------------------------
		function deleteFileAtIndex(arg0_file_path) {
			const local_file_path = arg0_file_path;
			if (local_file_path === undefined) {
				// All done
				if (callback_function !== undefined) callback_function(dialog_window);
				return;
			}
			
			try {
				const file_index = file_paths.indexOf(local_file_path);
				
				// Update overall progress
				const files_progress_bar_el = html_el.querySelector(`progress#files-progress`);
				const files_progress_label_el = html_el.querySelector(`label[for="files-progress"]`);
				files_progress_bar_el.value = ((file_index + 1) / file_paths.length) * 100;
				files_progress_label_el.innerHTML = loc("ve.registry.localisation.FileExplorer_of_progress", String.formatNumber(file_index + 1), String.formatNumber(file_paths.length));
				
				// Delete existing file/folder if present
				if (fs.existsSync(local_file_path)) {
					deleteWithProgress(local_file_path).then(() => {
						scheduleNextFile(file_index + 1);
					});
				} else {
					// File doesn't exist, skip to next
					scheduleNextFile(file_index + 1);
				}
			} catch (e) {
				let local_error_div = document.createElement("div");
					local_error_div.innerHTML = `<div class = "error">ERROR: ${e.stack}</div>`;
				dialog_window.html.element.appendChild(local_error_div);
			}
			
			function scheduleNextFile(next_index) {
				setTimeout(() => deleteFileAtIndex(file_paths[next_index]), 0);
			}
		}
		
		// ---------------------------------------------------------
		// Helper: delete a file or folder, show per‑file progress
		// ---------------------------------------------------------
		async function deleteWithProgress(target_path) {
			html_el.querySelector(`#current-file`).innerHTML = loc("ve.registry.localisation.FileExplorer_deleting_path", target_path);
			
			let stats;
			try {
				stats = fs.statSync(target_path);
			} catch (err) {
				renderFileProgress(1, 0);
				return;
			}
			
			if (stats.isDirectory()) {
				// Recursively delete subentries
				const entries = await fs.promises.readdir(target_path, { withFileTypes: true });
				for (const entry of entries) {
					const subPath = path.join(target_path, entry.name);
					await deleteWithProgress(subPath);
				}
				
				// After contents removed, remove directory itself
				await fs.promises.rmdir(target_path);
				renderFileProgress(1, 0);
			} else {
				// File case
				renderFileProgress(0.5, 0.05);
				await fs.promises.unlink(target_path);
				renderFileProgress(1, 0);
			}
		}
		
		// ---------------------------------------------------------
		// Helper: render single file progress
		// ---------------------------------------------------------
		function renderFileProgress(percent_progress, eta_seconds) {
			const file_progress_bar_el = html_el.querySelector(`progress#file-progress`);
			const file_progress_label_el = html_el.querySelector(`label[for="file-progress"]`);
			
			file_progress_bar_el.value = percent_progress * 100;
			file_progress_label_el.innerHTML =
				eta_seconds > 0
					? loc("ve.registry.localisation.FileExplorer_eta_remaining", Math.round(file_progress_bar_el.value), String.formatDateLength(Math.ceil(eta_seconds)))  :
					loc("ve.registry.localisation.FileExplorer_done");
		}
	};
	
	/**
	 * Returns all the subpaths in the set of file/folder paths given.
	 * - Method of: {@link ve.FileExplorer}
	 *
	 * @alias #getFiles
	 * @memberof ve.Component.ve.FileExplorer
	 *
	 * @param {string[]} arg0_file_paths
	 *
	 * @returns {string[]}
	 */
	ve.FileExplorer.getFiles = function (arg0_file_paths) {
		//Convert from parameters
		let file_paths = arg0_file_paths;
		
		//Declare local instance variables
		let actual_paths = [];
		
		//Iterate over all file_paths
		for (let i = 0; i < file_paths.length; i++)
			if (fs.statSync(file_paths[i]).isDirectory()) {
				//Iterate over all_file_paths in local directory 
				let all_file_paths = fs.readdirSync(file_paths[i], { recursive: true });
				
				//Iterates over all_file_paths to recursively 
				for (let x = 0; x < all_file_paths.length; x++)
					actual_paths.push(path.join(file_paths[i], all_file_paths[x]));
			} else {
				actual_paths.push(file_paths[i]);
			}
		
		//Return statement
		return actual_paths;
	};
	
	/**
	 * Moves files from selected file paths in `arg0_file_paths` to `arg1_file_path`.
	 * - Method of: {@link ve.FileExplorer}
	 *
	 * @alias #move
	 * @memberof ve.Component.ve.FileExplorer
	 * 
	 * @param {string[]} arg0_file_paths - The file paths to move.
	 * @param {string} arg1_file_path - The folder path to move to.
	 * @param {function} arg2_function - Callback function once the move operation is finished.
	 */
	ve.FileExplorer.move = function (arg0_file_paths, arg1_file_path, arg2_function) {
		//Convert from parameters
		let file_paths = arg0_file_paths;
		let file_path = arg1_file_path;
		let callback_function = arg2_function;
		
		//Declare local instance variables
		let files_total = String.formatNumber(ve.FileExplorer.getFiles(file_paths).length);
		
		//1. Copy files into new folder first
		ve.FileExplorer.copy(file_paths, file_path, (local_modal) => {
			local_modal.remove();
			
			//2. Delete files afterwards
			ve.FileExplorer.delete(file_paths, (local_modal) => {
				local_modal.remove();
				
				let modal = new ve.Window(loc("ve.registry.localisation.FileExplorer_moved_files", files_total, file_path), { 
					name: loc("ve.registry.localisation.FileExplorer_finished_moving_files"), 
					width: "24rem" 
				});
				if (callback_function)
					callback_function(modal);
			});
		});
	};
	
	/**
	 * Creates a modal for renaming a specific file path in `arg0_file_path`.
	 * - Method of: {@link ve.FileExplorer}
	 *
	 * @alias #rename
	 * @memberof ve.Component.ve.FileExplorer
	 * 
	 * @param {string} arg0_file_path - The file path to rename.
	 * @param {function} arg1_function - Callback function after the rename operation is complete.
	 */
	ve.FileExplorer.rename = function (arg0_file_path, arg1_function) {
		//Convert from parameters
		let file_path = arg0_file_path;
		let callback_function = arg1_function;
		
		//Declare local instance variables
		let base_name = path.basename(file_path);
		let dir_name = path.dirname(file_path);
		let local_modal = new ve.Window({
			html: new ve.HTML(loc("ve.registry.localisation.FileExplorer_rename_file_to", base_name)),
			new_file_name: new ve.Text(base_name, { name: " " }),
			confirm_button: new ve.Button((e) => {
				try {
					let new_path = path.join(dir_name, local_modal.components_obj.new_file_name.v);
					fs.rename(file_path, new_path, () => {
						local_modal.remove();
						if (callback_function !== undefined)
							callback_function(local_modal);
					});
				} catch (e) {
					new ve.Toast(loc("ve.registry.localisation.FileExplorer_error_could_not_be_renamed", base_name, e));
				}
			})
		}, { name: loc("ve.registry.localisation.FileExplorer_rename_file"), width: "24rem" });
	};
}