feat(smartfs.directory): feat(smartfs.directory): add directory copy/move with conflict handling and options
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartfs',
|
||||
version: '1.2.0',
|
||||
version: '1.3.0',
|
||||
description: 'a cross platform extendable fs module'
|
||||
}
|
||||
|
||||
@@ -27,7 +27,18 @@ export class SmartFsDirectory {
|
||||
mode?: TFileMode;
|
||||
filter?: string | RegExp | ((entry: IDirectoryEntry) => boolean);
|
||||
includeStats?: boolean;
|
||||
} = {};
|
||||
// Copy/move options
|
||||
applyFilter?: boolean;
|
||||
overwrite?: boolean;
|
||||
preserveTimestamps?: boolean;
|
||||
onConflict?: 'merge' | 'error' | 'replace';
|
||||
} = {
|
||||
// Defaults for copy/move
|
||||
applyFilter: true,
|
||||
overwrite: false,
|
||||
preserveTimestamps: false,
|
||||
onConflict: 'merge',
|
||||
};
|
||||
|
||||
constructor(provider: ISmartFsProvider, path: string) {
|
||||
this.provider = provider;
|
||||
@@ -82,6 +93,46 @@ export class SmartFsDirectory {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control whether filter() is applied during copy/move operations
|
||||
* @param apply - If true, only copy/move files matching filter; if false, copy/move all files
|
||||
* @default true
|
||||
*/
|
||||
public applyFilter(apply: boolean = true): this {
|
||||
this.options.applyFilter = apply;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control whether to overwrite existing files during copy/move
|
||||
* @param overwrite - If true, overwrite existing files; if false, throw error on conflict
|
||||
* @default false
|
||||
*/
|
||||
public overwrite(overwrite: boolean = true): this {
|
||||
this.options.overwrite = overwrite;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control whether to preserve file timestamps during copy/move
|
||||
* @param preserve - If true, preserve original timestamps; if false, use current time
|
||||
* @default false
|
||||
*/
|
||||
public preserveTimestamps(preserve: boolean = true): this {
|
||||
this.options.preserveTimestamps = preserve;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control behavior when target directory already exists
|
||||
* @param behavior - 'merge' to merge contents, 'error' to throw, 'replace' to delete and recreate
|
||||
* @default 'merge'
|
||||
*/
|
||||
public onConflict(behavior: 'merge' | 'error' | 'replace'): this {
|
||||
this.options.onConflict = behavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
// --- Action Methods (return Promises) ---
|
||||
|
||||
/**
|
||||
@@ -132,6 +183,102 @@ export class SmartFsDirectory {
|
||||
return this.provider.directoryStat(this.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the directory to a new location
|
||||
* @param targetPath - Destination path
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic copy
|
||||
* await fs.directory('/source').copy('/target');
|
||||
*
|
||||
* // Copy with options
|
||||
* await fs.directory('/source')
|
||||
* .recursive()
|
||||
* .filter('*.ts')
|
||||
* .overwrite(true)
|
||||
* .preserveTimestamps(true)
|
||||
* .copy('/target');
|
||||
*
|
||||
* // Copy all files (ignore filter)
|
||||
* await fs.directory('/source')
|
||||
* .applyFilter(false)
|
||||
* .copy('/target');
|
||||
* ```
|
||||
*/
|
||||
public async copy(targetPath: string): Promise<void> {
|
||||
const normalizedTarget = this.provider.normalizePath(targetPath);
|
||||
|
||||
// Handle conflict behavior
|
||||
const targetExists = await this.provider.directoryExists(normalizedTarget);
|
||||
if (targetExists) {
|
||||
if (this.options.onConflict === 'error') {
|
||||
throw new Error(`EEXIST: directory already exists: ${normalizedTarget}`);
|
||||
}
|
||||
if (this.options.onConflict === 'replace') {
|
||||
await this.provider.deleteDirectory(normalizedTarget, { recursive: true });
|
||||
}
|
||||
// 'merge' (default) - continue and overwrite based on file settings
|
||||
}
|
||||
|
||||
// Create target directory
|
||||
await this.provider.createDirectory(normalizedTarget, { recursive: true });
|
||||
|
||||
// List entries (always recursive for copy, respects filter based on applyFilter option)
|
||||
const listOptions: IListOptions = {
|
||||
recursive: true,
|
||||
filter: this.options.applyFilter ? this.options.filter : undefined,
|
||||
includeStats: false,
|
||||
};
|
||||
const entries = await this.provider.listDirectory(this.path, listOptions);
|
||||
|
||||
// Process entries - sort to ensure directories are created before their contents
|
||||
const sortedEntries = entries.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
for (const entry of sortedEntries) {
|
||||
const relativePath = entry.path.substring(this.path.length);
|
||||
const targetEntryPath = this.provider.joinPath(normalizedTarget, relativePath);
|
||||
|
||||
if (entry.isDirectory) {
|
||||
await this.provider.createDirectory(targetEntryPath, { recursive: true });
|
||||
} else {
|
||||
// Ensure parent directory exists
|
||||
const parentPath = targetEntryPath.substring(0, targetEntryPath.lastIndexOf('/'));
|
||||
if (parentPath && parentPath !== normalizedTarget) {
|
||||
await this.provider.createDirectory(parentPath, { recursive: true });
|
||||
}
|
||||
// Copy file using provider
|
||||
await this.provider.copyFile(entry.path, targetEntryPath, {
|
||||
preserveTimestamps: this.options.preserveTimestamps,
|
||||
overwrite: this.options.overwrite,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the directory to a new location
|
||||
* @param targetPath - Destination path
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic move
|
||||
* await fs.directory('/source').move('/target');
|
||||
*
|
||||
* // Move with conflict handling
|
||||
* await fs.directory('/source')
|
||||
* .onConflict('replace')
|
||||
* .move('/target');
|
||||
* ```
|
||||
*/
|
||||
public async move(targetPath: string): Promise<void> {
|
||||
// Copy first using current configuration
|
||||
await this.copy(targetPath);
|
||||
|
||||
// Delete source (always recursive, regardless of filter - this matches mv behavior)
|
||||
await this.provider.deleteDirectory(this.path, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory path
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user