-
Notifications
You must be signed in to change notification settings - Fork 45
chore: a tiny bit safer approach #482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
0c11718
a11e966
f739149
acd9eef
17629fb
eaa717c
8d4d289
eed1d8f
d519966
c6dd180
1797ed9
1cadf72
190b266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| 'use strict'; | ||
|
|
||
| import assert from 'node:assert'; | ||
| import { mkdir, readFile, rm, utimes, writeFile } from 'node:fs/promises'; | ||
| import { join } from 'node:path'; | ||
| import { afterEach, beforeEach, describe, it } from 'node:test'; | ||
|
|
||
| import { safeCopy } from '../safeCopy.mjs'; | ||
|
|
||
| describe('safeCopy', () => { | ||
| const testDir = join(import.meta.dirname, 'test-safe-copy'); | ||
| const srcDir = join(testDir, 'src'); | ||
| const targetDir = join(testDir, 'target'); | ||
|
|
||
| beforeEach(async () => { | ||
| // Create test directories | ||
| await mkdir(srcDir, { recursive: true }); | ||
| await mkdir(targetDir, { recursive: true }); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| // Clean up test directories | ||
| await rm(testDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it('should copy new files that do not exist in target', async () => { | ||
| // Create a file in source | ||
| await writeFile(join(srcDir, 'file1.txt'), 'content1'); | ||
|
|
||
| await safeCopy(srcDir, targetDir); | ||
|
|
||
| // Verify file was copied | ||
| const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8'); | ||
| assert.strictEqual(content, 'content1'); | ||
| }); | ||
|
|
||
| it('should copy multiple files', async () => { | ||
| // Create multiple files in source | ||
| await writeFile(join(srcDir, 'file1.txt'), 'content1'); | ||
| await writeFile(join(srcDir, 'file2.txt'), 'content2'); | ||
| await writeFile(join(srcDir, 'file3.txt'), 'content3'); | ||
|
|
||
| await safeCopy(srcDir, targetDir); | ||
|
|
||
| // Verify all files were copied | ||
| const content1 = await readFile(join(targetDir, 'file1.txt'), 'utf-8'); | ||
| const content2 = await readFile(join(targetDir, 'file2.txt'), 'utf-8'); | ||
| const content3 = await readFile(join(targetDir, 'file3.txt'), 'utf-8'); | ||
|
|
||
| assert.strictEqual(content1, 'content1'); | ||
| assert.strictEqual(content2, 'content2'); | ||
| assert.strictEqual(content3, 'content3'); | ||
| }); | ||
|
|
||
| it('should skip files with same size and older modification time', async () => { | ||
| // Create file in source with specific size | ||
| const content = 'same content'; | ||
| await writeFile(join(srcDir, 'file1.txt'), content); | ||
|
|
||
| // Make source file old | ||
| const oldTime = new Date(Date.now() - 10000); | ||
| await utimes(join(srcDir, 'file1.txt'), oldTime, oldTime); | ||
|
|
||
| // Create target file with same size but different content and newer timestamp | ||
| await writeFile(join(targetDir, 'file1.txt'), 'other things'); | ||
|
|
||
| await safeCopy(srcDir, targetDir); | ||
|
Check failure on line 67 in src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
|
||
|
|
||
| // Verify file was not overwritten (source is older) | ||
| const targetContent = await readFile(join(targetDir, 'file1.txt'), 'utf-8'); | ||
| assert.strictEqual(targetContent, 'other things'); | ||
| }); | ||
|
|
||
| it('should copy files when source has newer modification time', async () => { | ||
| // Create files in both directories | ||
| await writeFile(join(srcDir, 'file1.txt'), 'new content'); | ||
| await writeFile(join(targetDir, 'file1.txt'), 'old content'); | ||
|
|
||
| // Make target file older | ||
| const oldTime = new Date(Date.now() - 10000); | ||
| await utimes(join(targetDir, 'file1.txt'), oldTime, oldTime); | ||
|
|
||
| await safeCopy(srcDir, targetDir); | ||
|
Check failure on line 83 in src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
|
||
|
|
||
| // Verify file was updated | ||
| const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8'); | ||
| assert.strictEqual(content, 'new content'); | ||
| }); | ||
|
|
||
| it('should copy files when sizes differ', async () => { | ||
| // Create files with different sizes | ||
| await writeFile(join(srcDir, 'file1.txt'), 'short'); | ||
| await writeFile(join(targetDir, 'file1.txt'), 'much longer content'); | ||
|
|
||
| await safeCopy(srcDir, targetDir); | ||
|
Check failure on line 95 in src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
|
||
|
|
||
| // Verify file was updated | ||
| const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8'); | ||
| assert.strictEqual(content, 'short'); | ||
| }); | ||
|
|
||
| it('should handle empty source directory', async () => { | ||
| // Don't create any files in source | ||
| await safeCopy(srcDir, targetDir); | ||
|
|
||
| // Verify no error occurred and target is still empty | ||
| const files = await readFile(targetDir).catch(() => []); | ||
| assert.ok(Array.isArray(files) || files === undefined); | ||
| }); | ||
|
|
||
| it('should copy files with same size but different content when mtime is newer', async () => { | ||
| // Create files with same size but different content | ||
| await writeFile(join(srcDir, 'file1.txt'), 'abcde'); | ||
| await writeFile(join(targetDir, 'file1.txt'), 'fghij'); | ||
|
|
||
| // Make target older | ||
| const oldTime = new Date(Date.now() - 10000); | ||
| await utimes(join(targetDir, 'file1.txt'), oldTime, oldTime); | ||
|
|
||
| await safeCopy(srcDir, targetDir); | ||
|
Check failure on line 120 in src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
|
||
|
|
||
| // Verify file was updated with source content | ||
| const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8'); | ||
| assert.strictEqual(content, 'abcde'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,43 @@ | ||
| 'use strict'; | ||
|
|
||
| import { readFile, writeFile, stat, readdir } from 'node:fs/promises'; | ||
| import { copyFile, readdir, stat, constants } from 'node:fs/promises'; | ||
| import { join } from 'node:path'; | ||
|
|
||
| /** | ||
| * Safely copies files from source to target directory, skipping files that haven't changed | ||
| * based on file stats (size and modification time) | ||
| * Attempts to copy a file forcibly (`COPYFILE_FICLONE_FORCE`. Otherwise, falls back to a time-based check approach) | ||
| * | ||
| * @param {string} srcDir - Source directory path | ||
| * @param {string} targetDir - Target directory path | ||
| */ | ||
| export async function safeCopy(srcDir, targetDir) { | ||
| const files = await readdir(srcDir); | ||
| try { | ||
|
aduh95 marked this conversation as resolved.
Outdated
|
||
| await copyFile(srcDir, targetDir, constants.COPYFILE_FICLONE); | ||
| } catch (err) { | ||
| if (err?.syscall !== 'copyfile') { | ||
| throw err; | ||
| } | ||
|
|
||
| for (const file of files) { | ||
| const sourcePath = join(srcDir, file); | ||
| const targetPath = join(targetDir, file); | ||
| const files = await readdir(srcDir); | ||
|
|
||
| const [sStat, tStat] = await Promise.allSettled([ | ||
| stat(sourcePath), | ||
| stat(targetPath), | ||
| ]); | ||
| for (const file of files) { | ||
| const sourcePath = join(srcDir, file); | ||
| const targetPath = join(targetDir, file); | ||
|
|
||
| const shouldWrite = | ||
| tStat.status === 'rejected' || | ||
| sStat.value.size !== tStat.value.size || | ||
| sStat.value.mtimeMs > tStat.value.mtimeMs; | ||
| const [sStat, tStat] = await Promise.all([ | ||
| stat(sourcePath), | ||
| stat(targetPath), | ||
| ]).catch(() => []); | ||
|
|
||
| if (!shouldWrite) { | ||
| continue; | ||
| } | ||
| const shouldWrite = | ||
| !tStat || | ||
| sStat.value.size !== tStat.value.size || | ||
| sStat.value.mtimeMs > tStat.value.mtimeMs; | ||
|
avivkeller marked this conversation as resolved.
Outdated
|
||
|
|
||
| const fileContent = await readFile(sourcePath); | ||
| if (!shouldWrite) { | ||
| continue; | ||
| } | ||
|
|
||
| await writeFile(targetPath, fileContent); | ||
| await copyFile(sourcePath, targetPath); | ||
|
ovflowd marked this conversation as resolved.
Outdated
|
||
| } | ||
|
ovflowd marked this conversation as resolved.
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.