Skip to content

Commit

Permalink
Add ability to compare files and symlinks; Rename function compare
Browse files Browse the repository at this point in the history
…to `compareDirectories`
  • Loading branch information
hugoalh committed Jan 21, 2025
1 parent 3f0e52d commit 0ec0cdd
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ An ES (JavaScript & TypeScript) module for enhanced file system operation.
## 🧩 APIs

- ```ts
function compare(oldPath: string | URL, newPath: string | URL): Promise<FSCompareResult>;
function compareDirectories(oldPath: string | URL, newPath: string | URL): Promise<FSCompareDirectoriesResult>;
```
- ```ts
function emptyDir(path: string | URL): Promise<void>;
Expand Down
225 changes: 186 additions & 39 deletions compare.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,146 @@
import { readFileAsChunks } from "./stream.ts";
import {
readFileAsChunks,
readFileAsChunksSync
} from "./stream.ts";
import {
walk,
walkSync,
type FSWalkEntryExtra
} from "./walk.ts";
export interface FSCompareResult {
created: string[];
modified: string[];
removed: string[];
}
async function compareFileIsModified(oldInfo: FSWalkEntryExtra, newInfo: FSWalkEntryExtra): Promise<boolean> {
if (oldInfo.size !== newInfo.size) {
return true;
/**
* Compare the files whether are different, asynchronously.
*
* > **🛡️ Runtime Permissions**
* >
* > - File System - Read \[Deno: `read`; NodeJS 🧪: `fs-read`\]
* > - *Resources*
* @param {string | URL} filePathA Path of the file A.
* @param {string | URL} filePathB Path of the file B.
* @returns {Promise<boolean>} Determine result.
*/
export async function compareFilesAreDifferent(filePathA: string | URL, filePathB: string | URL): Promise<boolean> {
const fileA: AsyncGenerator<Uint8Array> = readFileAsChunks(filePathA, { reduceChunks: true });
const fileB: AsyncGenerator<Uint8Array> = readFileAsChunks(filePathB, { reduceChunks: true });
let done: boolean = false;
let result: boolean = false;
while (!done) {
const [
fileAChunkInfo,
fileBChunkInfo
]: [IteratorResult<Uint8Array>, IteratorResult<Uint8Array>] = await Promise.all([
fileA.next(),
fileB.next()
]);
if (
(fileAChunkInfo.done && !fileBChunkInfo.done) ||
(!fileAChunkInfo.done && fileBChunkInfo.done) ||
(fileAChunkInfo.value?.length !== fileBChunkInfo.value?.length)
) {
result = true;
break;
}
const indexLength: number = Math.max(fileAChunkInfo.value?.length ?? 0, fileBChunkInfo.value?.length ?? 0);
for (let index: number = 0; index < indexLength; index += 1) {
if ((fileAChunkInfo.value as Uint8Array)[index] !== (fileBChunkInfo.value as Uint8Array)[index]) {
result = true;
break;
}
}
done = (fileAChunkInfo.done ?? false) && (fileBChunkInfo.done ?? false);
}
const oldFile: AsyncGenerator<Uint8Array> = readFileAsChunks(oldInfo.pathAbsolute, { reduceChunks: true });
const newFile: AsyncGenerator<Uint8Array> = readFileAsChunks(newInfo.pathAbsolute, { reduceChunks: true });
await Promise.all([
fileA.return(undefined),
fileB.return(undefined)
]);
return result;
}
/**
* Compare the files whether are different, synchronously.
*
* > **🛡️ Runtime Permissions**
* >
* > - File System - Read \[Deno: `read`; NodeJS 🧪: `fs-read`\]
* > - *Resources*
* @param {string | URL} filePathA Path of the file A.
* @param {string | URL} filePathB Path of the file B.
* @returns {boolean} Determine result.
*/
export function compareFilesAreDifferentSync(filePathA: string | URL, filePathB: string | URL): boolean {
const fileA: Generator<Uint8Array> = readFileAsChunksSync(filePathA, { reduceChunks: true });
const fileB: Generator<Uint8Array> = readFileAsChunksSync(filePathB, { reduceChunks: true });
let done: boolean = false;
let result: boolean = false;
while (!done) {
const oldChunkInfo: IteratorResult<Uint8Array> = await oldFile.next();
const newChunkInfo: IteratorResult<Uint8Array> = await newFile.next();
const fileAChunkInfo: IteratorResult<Uint8Array> = fileA.next();
const fileBChunkInfo: IteratorResult<Uint8Array> = fileB.next();
if (
(oldChunkInfo.done && !newChunkInfo.done) ||
(!oldChunkInfo.done && newChunkInfo.done) ||
(oldChunkInfo.value?.length !== newChunkInfo.value?.length)
(fileAChunkInfo.done && !fileBChunkInfo.done) ||
(!fileAChunkInfo.done && fileBChunkInfo.done) ||
(fileAChunkInfo.value?.length !== fileBChunkInfo.value?.length)
) {
await oldFile.return(undefined);
await newFile.return(undefined);
return true;
result = true;
break;
}
const indexLength: number = Math.max(oldChunkInfo.value?.length ?? 0, newChunkInfo.value?.length ?? 0);
const indexLength: number = Math.max(fileAChunkInfo.value?.length ?? 0, fileBChunkInfo.value?.length ?? 0);
for (let index: number = 0; index < indexLength; index += 1) {
if ((oldChunkInfo.value as Uint8Array)[index] !== (newChunkInfo.value as Uint8Array)[index]) {
await oldFile.return(undefined);
await newFile.return(undefined);
return true;
if ((fileAChunkInfo.value as Uint8Array)[index] !== (fileBChunkInfo.value as Uint8Array)[index]) {
result = true;
break;
}
}
done = (oldChunkInfo.done ?? false) && (newChunkInfo.done ?? false);
done = (fileAChunkInfo.done ?? false) && (fileBChunkInfo.done ?? false);
}
await oldFile.return(undefined);
await newFile.return(undefined);
return false;
fileA.return(undefined);
fileB.return(undefined);
return result;
}
async function compareSymlinkIsModified(oldInfo: FSWalkEntryExtra, newInfo: FSWalkEntryExtra): Promise<boolean> {
/**
* Compare the symlinks whether are different, asynchronously.
*
* > **🛡️ Runtime Permissions**
* >
* > - File System - Read \[Deno: `read`; NodeJS 🧪: `fs-read`\]
* > - *Resources*
* @param {string | URL} symlinkPathA Path of the symlink A.
* @param {string | URL} symlinkPathB Path of the symlink B.
* @returns {Promise<boolean>} Determine result.
*/
export async function compareSymlinksAreDifferent(symlinkPathA: string | URL, symlinkPathB: string | URL): Promise<boolean> {
const [
oldLink,
newLink
symlinkA,
symlinkB
]: [string, string] = await Promise.all([
Deno.readLink(oldInfo.pathAbsolute),
Deno.readLink(newInfo.pathAbsolute)
Deno.readLink(symlinkPathA),
Deno.readLink(symlinkPathB)
]);
return (oldLink === newLink);
return (symlinkA !== symlinkB);
}
/**
* Compare directories.
* Compare the symlinks whether are different, synchronously.
*
* > **🛡️ Runtime Permissions**
* >
* > - File System - Read \[Deno: `read`; NodeJS 🧪: `fs-read`\]
* > - *Resources*
* @param {string | URL} symlinkPathA Path of the symlink A.
* @param {string | URL} symlinkPathB Path of the symlink B.
* @returns {boolean} Determine result.
*/
export function compareSymlinksAreDifferentSync(symlinkPathA: string | URL, symlinkPathB: string | URL): boolean {
return ((Deno.readLinkSync(symlinkPathA)) !== (Deno.readLinkSync(symlinkPathB)));
}
export interface FSCompareDirectoriesResult {
created: string[];
modified: string[];
removed: string[];
}
/**
* Compare the differences between the directories, asynchronously.
* @param {string | URL} oldPath Path of the old directory.
* @param {string | URL} newPath Path of the new directory.
* @returns {Promise<FSCompareResult>} Result of the compare.
* @returns {Promise<FSCompareDirectoriesResult>} Result of the compare.
*/
export async function compare(oldPath: string | URL, newPath: string | URL): Promise<FSCompareResult> {
export async function compareDirectories(oldPath: string | URL, newPath: string | URL): Promise<FSCompareDirectoriesResult> {
const [
oldStatL,
newStatL
Expand Down Expand Up @@ -91,14 +174,78 @@ export async function compare(oldPath: string | URL, newPath: string | URL): Pro
if (oldEntry.isDirectory && newEntryFound.isDirectory) {
// Do nothing.
} else if (oldEntry.isFile && newEntryFound.isFile) {
if (await compareFileIsModified(oldEntry, newEntryFound)) {
if (await compareFilesAreDifferent(oldEntry.pathAbsolute, newEntryFound.pathAbsolute)) {
modified.push(newEntryFound);
}
} else if (
(oldEntry.isSymlinkDirectory && newEntryFound.isSymlinkDirectory) ||
(oldEntry.isSymlinkFile && newEntryFound.isSymlinkFile)
) {
if (await compareSymlinksAreDifferent(oldEntry.pathAbsolute, newEntryFound.pathAbsolute)) {
modified.push(newEntryFound);
}
} else {
modified.push(newEntryFound);
}
}
}
for (const newEntry of newEntries) {
if (oldEntries.findIndex((oldEntry: FSWalkEntryExtra): boolean => {
return (newEntry.pathRelative === oldEntry.pathRelative);
}) === -1) {
created.push(newEntry);
}
}
return {
created: created.map(({ pathRelative }: FSWalkEntryExtra): string => {
return pathRelative;
}),
modified: modified.map(({ pathRelative }: FSWalkEntryExtra): string => {
return pathRelative;
}),
removed: removed.map(({ pathRelative }: FSWalkEntryExtra): string => {
return pathRelative;
})
};
}
/**
* Compare the differences between the directories, synchronously.
* @param {string | URL} oldPath Path of the old directory.
* @param {string | URL} newPath Path of the new directory.
* @returns {FSCompareDirectoriesResult} Result of the compare.
*/
export function compareDirectoriesSync(oldPath: string | URL, newPath: string | URL): FSCompareDirectoriesResult {
const oldStatL: Deno.FileInfo = Deno.lstatSync(oldPath);
const newStatL: Deno.FileInfo = Deno.lstatSync(newPath);
if (!oldStatL.isDirectory) {
throw new Deno.errors.NotADirectory(`Path \`${oldPath}\` (parameter \`oldPath\`) is not a directory!`);
}
if (!newStatL.isDirectory) {
throw new Deno.errors.NotADirectory(`Path \`${newPath}\` (parameter \`newPath\`) is not a directory!`);
}
const oldEntries: FSWalkEntryExtra[] = Array.from(walkSync(oldPath, { extraInfo: true }));
const newEntries: FSWalkEntryExtra[] = Array.from(walkSync(newPath, { extraInfo: true }));
const created: FSWalkEntryExtra[] = [];
const modified: FSWalkEntryExtra[] = [];
const removed: FSWalkEntryExtra[] = [];
for (const oldEntry of oldEntries) {
const newEntryFound: FSWalkEntryExtra | undefined = newEntries.find((newEntry: FSWalkEntryExtra): boolean => {
return (oldEntry.pathRelative === newEntry.pathRelative);
});
if (typeof newEntryFound === "undefined") {
removed.push(oldEntry);
} else {
if (oldEntry.isDirectory && newEntryFound.isDirectory) {
// Do nothing.
} else if (oldEntry.isFile && newEntryFound.isFile) {
if (compareFilesAreDifferentSync(oldEntry.pathAbsolute, newEntryFound.pathAbsolute)) {
modified.push(newEntryFound);
}
} else if (
(oldEntry.isSymlinkDirectory && newEntryFound.isSymlinkDirectory) ||
(oldEntry.isSymlinkFile && newEntryFound.isSymlinkFile)
) {
if (await compareSymlinkIsModified(oldEntry, newEntryFound)) {
if (compareSymlinksAreDifferentSync(oldEntry.pathAbsolute, newEntryFound.pathAbsolute)) {
modified.push(newEntryFound);
}
} else {
Expand Down
9 changes: 9 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export {
compareDirectories,
compareDirectoriesSync,
compareFilesAreDifferent,
compareFilesAreDifferentSync,
compareSymlinksAreDifferent,
compareSymlinksAreDifferentSync,
type FSCompareDirectoriesResult
} from "./compare.ts";
export {
getDriveInfo,
getDriveInfoSync,
Expand Down

0 comments on commit 0ec0cdd

Please sign in to comment.