@md/merge-folders@1.0.0Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
latest
m-dressler/merge-foldersAllows you to specify to folders which should be merged into one
This package works with Deno
JSR Score
94%
Published
a month ago (1.0.0)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131import { crypto } from "jsr:@std/crypto@^1.0.3"; import { iterateReader } from "jsr:/@std/io@^0.224.6/iterate-reader"; import { walk, type WalkEntry } from "jsr:/@std/fs@^1.0.2/walk"; import { resolve } from "jsr:/@std/path@^1.0.3/resolve"; /** Calculates a files unique hash using SHA256 */ const calcHash = async (path: string): Promise<Uint8Array> => { using file = await Deno.open(path); /** Can assert Uint32Array as SHA-256 returns 256 bits = 8 * 32 bits */ return new Uint8Array( await crypto.subtle.digest("SHA-256", iterateReader(file)) ); }; /** Checks if the contents of {@see path1 } and {@see path2 } have the same hash */ const sameHash = async (path1: string, path2: string) => { const hash1Promise = calcHash(path1); const hash2 = await calcHash(path2); const hash1 = await hash1Promise; for (let i = 0; i < hash1.length; ++i) if (hash1[i] !== hash2[i]) return false; return true; }; /** Cleans a folder recursively for each folder without non-folder children */ const removeEmptyFolders = async (folder: string) => { const { isDirectory } = await Deno.stat(folder); if (!isDirectory) throw new Error("Provided path is not a directory"); /** If the current folder is empty */ let isEmpty = true; const files = Deno.readDir(folder); for await (const entry of files) { const fullPath = resolve(folder, entry.name); // If there is at least one file, this directory is not empty if (!entry.isDirectory) isEmpty = false; // If it's a directory and not empty, neither is this folder else if (!(await removeEmptyFolders(fullPath))) isEmpty = false; } // If it is empty, remove this folder if (isEmpty) await Deno.remove(folder); return isEmpty; }; /** Walks a directory like {@see walk } but converts the {@see WalkEntry.name } property to the local path within the directory */ const directoryWalk = async (directory: string): Promise<WalkEntry[]> => { const entries: WalkEntry[] = []; for await (const entry of walk(directory)) { // If entry is the root directory entry, skip it if (entry.path === directory) continue; // Remove the directory path plus the slash entry.name = entry.path.substring(directory.length + 1); entries.push(entry); } return entries; }; /** * Merges contents from {@see toMerge } into {@see target } * * @returns All the entries which had merge conflicts */ export const mergeFolders = async (target: string, toMerge: string): Promise<WalkEntry[]> => { // This function iteratively traverses each folder/filer in `folder1` // and merges its content into the equivalent folder in `folder2` or // moves it there if no equivalent exists target = resolve(target); toMerge = resolve(toMerge); if (target === toMerge) throw new Error("Merge folder cannot be the same as merge target"); if (target.startsWith(toMerge) || toMerge.startsWith(target)) throw new Error( "Target and merge folders cannot be children of each other" ); /** Promise of all toMerge's children (not awaited yet for parallelism) */ const mergeChildrenPromise = directoryWalk(toMerge); /** All the target's children */ const targetChildren = await directoryWalk(target); /** All the files to merge into the new folder */ const mergeChildren = await mergeChildrenPromise; /** A dictionary of target children names to their full entries */ const targetDirectory = Object.fromEntries( targetChildren.map((c) => [c.name, c]) ); /** Any files which are present in folder1 and folder2 but have different hashes */ const conflicts: WalkEntry[] = []; for (let i = 0; i < mergeChildren.length; ++i) { const mergeChild = mergeChildren[i]; const targetChild = targetDirectory[mergeChild.name]; // If targetChild doesn't exist, just move it as it's net new if (!targetChild) { // If it is a directory, skip its children as they don't need to be merged if (mergeChild.isDirectory) { // Check if the path starts with the directories path and remove if so while (mergeChildren[i + 1]?.name.startsWith(mergeChild.name)) mergeChildren.splice(i + 1, 1); } const newPath = resolve(target, mergeChild.name); await Deno.rename(mergeChild.path, newPath); } else if (mergeChild.isDirectory && targetChild.isDirectory) { // If both are directories, we can ignore it } // If both are files, check if they are the same and can be merged else if (mergeChild.isFile && targetChild.isFile) { const sameFile = await sameHash(mergeChild.path, targetChild.path); // If they are the same, delete merge child as it already exists if (sameFile) await Deno.remove(mergeChild.path); // Otherwise return as conflict else conflicts.push(targetChild); } // If they are different types, return as conflict else conflicts.push(targetChild); } // Clean `toMerge` of empty directories await removeEmptyFolders(toMerge); return conflicts; };