Skip to content

Commit d2ae575

Browse files
committed
feat(deletion-strategy): add windows deletion strategies
1 parent 0de97e0 commit d2ae575

5 files changed

Lines changed: 162 additions & 7 deletions

File tree

src/core/services/files/strategies/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ export * from './unix-strategies/rsync-deletion.strategy.js';
33
export * from './unix-strategies/find-deletion.strategy.js';
44
export * from './unix-strategies/rm-rf-deletion.strategy.js';
55
export * from './unix-strategies/perl-deletion.strategy.js';
6+
export * from './windows-strategies/robocopy-deletion.strategy.js';
7+
export * from './windows-strategies/powershell-deletion.strategy.js';
8+
export * from './windows-strategies/node-rm-deletion.strategy.js';
69
export * from './strategy-manager.js';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { rm } from 'fs/promises';
2+
import { IDeletionStrategy } from '../deletion-strategy.interface.js';
3+
4+
export class NodeRmDeletionStrategy implements IDeletionStrategy {
5+
readonly name = 'node-rm';
6+
7+
async isAvailable(): Promise<boolean> {
8+
return Promise.resolve(true);
9+
}
10+
11+
async delete(path: string): Promise<boolean> {
12+
try {
13+
await rm(path, {
14+
recursive: true,
15+
force: true,
16+
// maxRetries: Automatically retry on Windows if files are locked
17+
maxRetries: process.platform === 'win32' ? 3 : 0,
18+
retryDelay: 100,
19+
});
20+
return true;
21+
} catch (error) {
22+
// If the directory doesn't exist, consider it a success
23+
if (error && (error as any).code === 'ENOENT') {
24+
return true;
25+
}
26+
throw error;
27+
}
28+
}
29+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { exec } from 'child_process';
2+
import { IDeletionStrategy } from '../deletion-strategy.interface.js';
3+
4+
export class PowerShellDeletionStrategy implements IDeletionStrategy {
5+
readonly name = 'powershell';
6+
7+
async isAvailable(): Promise<boolean> {
8+
return new Promise((resolve) => {
9+
exec('powershell -Command "Get-Command Remove-Item" 2>nul', (error) => {
10+
resolve(error === null);
11+
});
12+
});
13+
}
14+
15+
async delete(path: string): Promise<boolean> {
16+
return new Promise((resolve, reject) => {
17+
// Use PowerShell with optimized flags for fast deletion
18+
// -Force: Override read-only and hidden attributes
19+
// -Recurse: Delete all child items
20+
// -ErrorAction Stop: Stop on first error
21+
// Get-ChildItem | Remove-Item pattern is often faster than Remove-Item alone for large dirs
22+
const psCommand = `
23+
if (Test-Path '${path}') {
24+
try {
25+
Get-ChildItem -Path '${path}' -Force -Recurse | Remove-Item -Force -Recurse -ErrorAction Stop
26+
Remove-Item -Path '${path}' -Force -ErrorAction Stop
27+
Write-Output 'Success'
28+
} catch {
29+
Write-Error $_.Exception.Message
30+
exit 1
31+
}
32+
} else {
33+
Write-Output 'Success'
34+
}
35+
`
36+
.replace(/\s+/g, ' ')
37+
.trim();
38+
39+
const command = `powershell -Command "${psCommand}"`;
40+
41+
exec(command, (error, stdout, stderr) => {
42+
if (error !== null) {
43+
reject(error);
44+
return;
45+
}
46+
if (stderr && stderr.trim() !== '') {
47+
reject(new Error(stderr));
48+
return;
49+
}
50+
resolve(true);
51+
});
52+
});
53+
}
54+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { exec } from 'child_process';
2+
import { IDeletionStrategy } from '../deletion-strategy.interface.js';
3+
4+
export class RobocopyDeletionStrategy implements IDeletionStrategy {
5+
readonly name = 'robocopy';
6+
7+
async isAvailable(): Promise<boolean> {
8+
return new Promise((resolve) => {
9+
exec('robocopy /? >nul 2>&1', (error) => {
10+
resolve(error === null || error.code === 16); // robocopy returns 16 for help
11+
});
12+
});
13+
}
14+
15+
async delete(path: string): Promise<boolean> {
16+
return new Promise((resolve, reject) => {
17+
// Create a temporary empty directory
18+
const tempDir = `${process.env.TEMP || process.env.TMP || 'C:\\temp'}\\npkill_empty_${Date.now()}`;
19+
20+
// Use robocopy to mirror empty directory to target (purge), then remove both
21+
// /MIR = Mirror directory tree (equivalent to /E plus /PURGE)
22+
// /NFL = No File List (don't log file names)
23+
// /NDL = No Directory List (don't log directory names)
24+
// /NJH = No Job Header
25+
// /NJS = No Job Summary
26+
// /NP = No Progress
27+
const command = `mkdir "${tempDir}" && robocopy "${tempDir}" "${path}" /MIR /NFL /NDL /NJH /NJS /NP && rmdir "${path}" && rmdir "${tempDir}"`;
28+
29+
exec(command, (error, stdout, stderr) => {
30+
// Robocopy returns different exit codes that aren't necessarily errors
31+
// 0 = No files copied, no failures
32+
// 1 = Files copied successfully
33+
// 2 = Extra files or directories detected
34+
// Exit codes 0-7 are generally successful
35+
if (error && error.code && error.code > 7) {
36+
reject(error);
37+
return;
38+
}
39+
if (stderr && stderr.trim() !== '') {
40+
reject(new Error(stderr));
41+
return;
42+
}
43+
resolve(true);
44+
});
45+
});
46+
}
47+
}
Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
1-
import { Subject, Observable } from 'rxjs';
21
import { FileService } from './files.service.js';
32
import { FileWorkerService } from './files.worker.service.js';
4-
import { ScanOptions } from '@core/index.js';
53
import { StreamService } from '../stream.service.js';
6-
import { rm } from 'fs/promises';
7-
import { DeletionStrategyManager } from './strategies/index.js';
4+
import { DeletionStrategyManager } from './strategies/strategy-manager.js';
5+
import {
6+
RobocopyDeletionStrategy,
7+
PowerShellDeletionStrategy,
8+
NodeRmDeletionStrategy,
9+
} from './strategies/index.js';
810

911
export class WindowsFilesService extends FileService {
1012
constructor(
11-
private readonly streamService: StreamService,
13+
protected streamService: StreamService,
1214
public override fileWorkerService: FileWorkerService,
1315
delstrategyManager: DeletionStrategyManager,
1416
) {
1517
super(fileWorkerService, delstrategyManager);
18+
this.initializeStrategies();
1619
}
1720

1821
async deleteDir(path: string): Promise<boolean> {
19-
await rm(path, { recursive: true, force: true });
20-
return true;
22+
try {
23+
return await this.delStrategyManager.deleteDirectory(path);
24+
} catch (error) {
25+
throw new Error(`Failed to delete directory ${path}: ${error}`);
26+
}
27+
}
28+
29+
getSelectedDeletionStrategy(): string | null {
30+
const strategy = this.delStrategyManager.getSelectedStrategy();
31+
return strategy ? strategy.name : null;
32+
}
33+
34+
resetDeletionStrategy(): void {
35+
this.delStrategyManager.resetStrategy();
36+
}
37+
38+
private initializeStrategies() {
39+
// Order matters!
40+
this.delStrategyManager.registerStrategy(new RobocopyDeletionStrategy());
41+
this.delStrategyManager.registerStrategy(new PowerShellDeletionStrategy());
42+
this.delStrategyManager.registerStrategy(new NodeRmDeletionStrategy());
2143
}
2244
}

0 commit comments

Comments
 (0)