feat(smartfs.directory): feat(smartfs.directory): add directory copy/move with conflict handling and options

This commit is contained in:
2025-12-16 10:02:36 +00:00
parent 69ce0f4e64
commit b764942183
5 changed files with 352 additions and 7 deletions

View File

@@ -367,6 +367,189 @@ tap.test('treeHash of empty directory should return consistent hash', async () =
expect(hash1.length).toEqual(64);
});
// --- Directory copy/move tests ---
tap.test('should copy a directory', async () => {
const sourcePath = path.join(tempDir, 'copy-dir-source');
const destPath = path.join(tempDir, 'copy-dir-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file1.txt')).write('content1');
await smartFs.file(path.join(sourcePath, 'file2.txt')).write('content2');
await smartFs.directory(sourcePath).copy(destPath);
// Source should still exist
const sourceExists = await smartFs.directory(sourcePath).exists();
expect(sourceExists).toEqual(true);
// Destination should exist with same files
const destExists = await smartFs.directory(destPath).exists();
expect(destExists).toEqual(true);
const destContent1 = await smartFs.file(path.join(destPath, 'file1.txt')).encoding('utf8').read();
const destContent2 = await smartFs.file(path.join(destPath, 'file2.txt')).encoding('utf8').read();
expect(destContent1).toEqual('content1');
expect(destContent2).toEqual('content2');
});
tap.test('should copy a directory recursively with nested subdirectories', async () => {
const sourcePath = path.join(tempDir, 'copy-recursive-source');
const destPath = path.join(tempDir, 'copy-recursive-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'root.txt')).write('root');
await smartFs.directory(path.join(sourcePath, 'sub1')).create();
await smartFs.file(path.join(sourcePath, 'sub1', 'nested1.txt')).write('nested1');
await smartFs.directory(path.join(sourcePath, 'sub1', 'sub2')).create();
await smartFs.file(path.join(sourcePath, 'sub1', 'sub2', 'deep.txt')).write('deep');
await smartFs.directory(sourcePath).copy(destPath);
// Verify all files copied
expect(await smartFs.file(path.join(destPath, 'root.txt')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'sub1', 'nested1.txt')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'sub1', 'sub2', 'deep.txt')).exists()).toEqual(true);
const deepContent = await smartFs.file(path.join(destPath, 'sub1', 'sub2', 'deep.txt')).encoding('utf8').read();
expect(deepContent).toEqual('deep');
});
tap.test('should copy directory with filter applied', async () => {
const sourcePath = path.join(tempDir, 'copy-filter-source');
const destPath = path.join(tempDir, 'copy-filter-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.ts')).write('typescript');
await smartFs.file(path.join(sourcePath, 'file.js')).write('javascript');
await smartFs.file(path.join(sourcePath, 'file.txt')).write('text');
// Copy only .ts files
await smartFs.directory(sourcePath).filter(/\.ts$/).copy(destPath);
expect(await smartFs.file(path.join(destPath, 'file.ts')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'file.js')).exists()).toEqual(false);
expect(await smartFs.file(path.join(destPath, 'file.txt')).exists()).toEqual(false);
});
tap.test('should copy all files when applyFilter(false)', async () => {
const sourcePath = path.join(tempDir, 'copy-no-filter-source');
const destPath = path.join(tempDir, 'copy-no-filter-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.ts')).write('typescript');
await smartFs.file(path.join(sourcePath, 'file.js')).write('javascript');
// Filter is set but applyFilter(false) ignores it
await smartFs.directory(sourcePath).filter(/\.ts$/).applyFilter(false).copy(destPath);
expect(await smartFs.file(path.join(destPath, 'file.ts')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'file.js')).exists()).toEqual(true);
});
tap.test('should copy with overwrite(true) replacing existing files', async () => {
const sourcePath = path.join(tempDir, 'copy-overwrite-source');
const destPath = path.join(tempDir, 'copy-overwrite-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.txt')).write('new content');
await smartFs.directory(destPath).create();
await smartFs.file(path.join(destPath, 'file.txt')).write('old content');
await smartFs.directory(sourcePath).overwrite(true).copy(destPath);
const content = await smartFs.file(path.join(destPath, 'file.txt')).encoding('utf8').read();
expect(content).toEqual('new content');
});
tap.test('should throw error when onConflict is error and target exists', async () => {
const sourcePath = path.join(tempDir, 'copy-conflict-error-source');
const destPath = path.join(tempDir, 'copy-conflict-error-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.txt')).write('content');
await smartFs.directory(destPath).create();
let threw = false;
try {
await smartFs.directory(sourcePath).onConflict('error').copy(destPath);
} catch (e: any) {
threw = true;
expect(e.message).toInclude('EEXIST');
}
expect(threw).toEqual(true);
});
tap.test('should replace target when onConflict is replace', async () => {
const sourcePath = path.join(tempDir, 'copy-replace-source');
const destPath = path.join(tempDir, 'copy-replace-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'new.txt')).write('new');
await smartFs.directory(destPath).create();
await smartFs.file(path.join(destPath, 'old.txt')).write('old');
await smartFs.directory(sourcePath).onConflict('replace').copy(destPath);
// Old file should be gone, new file should exist
expect(await smartFs.file(path.join(destPath, 'old.txt')).exists()).toEqual(false);
expect(await smartFs.file(path.join(destPath, 'new.txt')).exists()).toEqual(true);
});
tap.test('should move a directory', async () => {
const sourcePath = path.join(tempDir, 'move-dir-source');
const destPath = path.join(tempDir, 'move-dir-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file1.txt')).write('content1');
await smartFs.file(path.join(sourcePath, 'file2.txt')).write('content2');
await smartFs.directory(sourcePath).move(destPath);
// Source should no longer exist
const sourceExists = await smartFs.directory(sourcePath).exists();
expect(sourceExists).toEqual(false);
// Destination should exist with files
const destExists = await smartFs.directory(destPath).exists();
expect(destExists).toEqual(true);
const destContent1 = await smartFs.file(path.join(destPath, 'file1.txt')).encoding('utf8').read();
expect(destContent1).toEqual('content1');
});
tap.test('should move directory recursively', async () => {
const sourcePath = path.join(tempDir, 'move-recursive-source');
const destPath = path.join(tempDir, 'move-recursive-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'root.txt')).write('root');
await smartFs.directory(path.join(sourcePath, 'sub')).create();
await smartFs.file(path.join(sourcePath, 'sub', 'nested.txt')).write('nested');
await smartFs.directory(sourcePath).move(destPath);
// Source should not exist
expect(await smartFs.directory(sourcePath).exists()).toEqual(false);
// All files should be at destination
expect(await smartFs.file(path.join(destPath, 'root.txt')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'sub', 'nested.txt')).exists()).toEqual(true);
});
tap.test('should copy empty directory', async () => {
const sourcePath = path.join(tempDir, 'copy-empty-source');
const destPath = path.join(tempDir, 'copy-empty-dest');
await smartFs.directory(sourcePath).create();
await smartFs.directory(sourcePath).copy(destPath);
expect(await smartFs.directory(destPath).exists()).toEqual(true);
});
tap.test('cleanup temp directory', async () => {
await fs.rm(tempDir, { recursive: true, force: true });
expect(true).toEqual(true);