From 5466ad61c35fc0b634877c683beb67ca3edc0012 Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Fri, 24 Jan 2025 22:42:24 +0100 Subject: [PATCH] Ensure file size consistency during folder content movement --- src/__tests__/unit/fs.helpers.test.ts | 92 +++++++++++++++++++++++++++ src/main/helpers/fs.helpers.ts | 44 +++++++++++++ src/shared/helpers/error.helpers.ts | 4 +- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/unit/fs.helpers.test.ts diff --git a/src/__tests__/unit/fs.helpers.test.ts b/src/__tests__/unit/fs.helpers.test.ts new file mode 100644 index 00000000..906e353a --- /dev/null +++ b/src/__tests__/unit/fs.helpers.test.ts @@ -0,0 +1,92 @@ +import { mkdir, pathExistsSync, rm, writeFile } from "fs-extra"; +import { getSize } from "main/helpers/fs.helpers"; +import path from "path"; + +const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "fs"); + +describe("Test fs.helpers getSize", () => { + + beforeEach(async () => { + if (pathExistsSync(TEST_FOLDER)) { + await rm(TEST_FOLDER, { recursive: true, force: true }); + } + await mkdir(TEST_FOLDER); + }); + + afterEach(async () => { + await rm(TEST_FOLDER, { recursive: true, force: true }); + }); + + it("should return 0 for empty folder", async () => { + const size = await getSize(TEST_FOLDER); + expect(size).toBe(0); + }); + + it("should throw error for non-existing folder", async () => { + await expect(getSize(`${TEST_FOLDER}1`)).rejects.toThrow(); + }); + + it("should return the total size of files in the directory", async () => { + const filePath = path.join(TEST_FOLDER, "testFile.bin"); + const buffer = Buffer.alloc(10); + + await writeFile(filePath, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(10); + }); + + it("should include the sizes of all files in the directory", async () => { + const filePath1 = path.join(TEST_FOLDER, "testFile1.bin"); + const filePath2 = path.join(TEST_FOLDER, "testFile2.bin"); + const buffer = Buffer.alloc(10); + + await writeFile(filePath1, buffer); + await writeFile(filePath2, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(20); + }); + + it("should include the sizes of files in nested directories", async () => { + const subFolder = path.join(TEST_FOLDER, "subFolder"); + const filePath1 = path.join(TEST_FOLDER, "testFile1.bin"); + const filePath2 = path.join(subFolder, "testFile2.bin"); + const buffer = Buffer.alloc(10); + + await mkdir(subFolder); + + await writeFile(filePath1, buffer); + await writeFile(filePath2, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(20); + }); + + it("should not include files beyond the default depth limit", async () => { + const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5"); + const filePath = path.join(subFolder, "testFile.bin"); + const buffer = Buffer.alloc(10); + + await mkdir(subFolder, { recursive: true }); + await writeFile(filePath, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(0); + }); + + it("should include files within the specified depth limit", async () => { + const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5"); + const filePath = path.join(subFolder, "testFile.bin"); + const filePath2 = path.join(TEST_FOLDER, "1", "2", "testFile2.bin"); + const buffer = Buffer.alloc(10); + + await mkdir(subFolder, { recursive: true }); + await writeFile(filePath, buffer); + await writeFile(filePath2, buffer); + + const size = await getSize(TEST_FOLDER, 6); + expect(size).toBe(20); + }); + +}); diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index d8111de9..5692d32f 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -81,6 +81,7 @@ export async function getFilesInFolder(folderPath: string): Promise { } export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable { + log.info(`(moveFolderContent) Moving ${src} to ${dest}`); const progress: Progression = { current: 0, total: 0 }; return new Observable(subscriber => { subscriber.next(progress); @@ -104,7 +105,17 @@ export function moveFolderContent(src: string, dest: string, option?: MoveOption const allChildsAlreadyExist = srcChilds.every(child => pathExistsSync(path.join(destFullPath, child))); if(file.isFile() || !allChildsAlreadyExist){ + const prevSize = await getSize(srcFullPath); await move(srcFullPath, destFullPath, option); + const afterSize = await getSize(destFullPath); + + // The size after moving should be the same or greater than the size before moving but never less + if(afterSize < prevSize){ + throw new CustomError(`File size mismath. before: ${prevSize}, after: ${afterSize} (${srcFullPath})`, "FILE_SIZE_MISMATCH"); + } + + } else { + log.info(`Skipping ${srcFullPath} to ${destFullPath}, all child already exist in destination`); } progress.current++; @@ -274,6 +285,39 @@ export function getUniqueFileNamePath(filePath: string): string { return path.join(dir, newFileName); } +/** + * @throws {Error} Can throw file system errors + */ +export async function getSize(targetPath: string, maxDepth = 5): Promise { + const visited = new Set(); + + const computeSize = async (currentPath: string, depth: number): Promise => { + if (visited.has(currentPath)){ + return 0; + } + + visited.add(currentPath); + + const stats = await stat(currentPath); + + if (stats.isFile()) { + return stats.size; + } + + if (!stats.isDirectory() || depth >= maxDepth) { + return 0; + } + + const entries = await readdir(currentPath); + const sizes = await Promise.all( + entries.map((entry) => computeSize(path.join(currentPath, entry), depth + 1)) + ); + return sizes.reduce((acc, cur) => acc + cur, 0); + }; + + return computeSize(targetPath, 0); +} + export interface Progression { total: number; current: number; diff --git a/src/shared/helpers/error.helpers.ts b/src/shared/helpers/error.helpers.ts index 33f71fce..a8fc2543 100644 --- a/src/shared/helpers/error.helpers.ts +++ b/src/shared/helpers/error.helpers.ts @@ -8,8 +8,8 @@ export function tryit(func: () => Return): TryitReturn { if(isPromise(result)){ return result - .then((value) => ({ error: null, result: value })) - .catch((err) => ({ error: err instanceof Error ? err : new Error(`${err}`), result: null })) as Return extends Promise + .then((value) => ({ error: null as null, result: value })) + .catch((err) => ({ error: err instanceof Error ? err : new Error(`${err}`), result: null as null })) as Return extends Promise ? Promise<{error: Error, result: undefined} | {error: undefined, result: Awaited}> : {error: Error, result: undefined} | {error: undefined, result: Return}; }