Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions deletion-speed-benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Unix

Check notice on line 1 in deletion-speed-benchmark.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

deletion-speed-benchmark.md#L1

Expected: [None]; Actual: # Unix

## Directorio grande

- Ficheros: 277866
- Tamaño: 4,35GB

### Benchmark implementado

- Método clásico (v0.12.2 - rm -rf): 9,90s
- Rsync: 14,82s
- Perl: 17,47s
- Find: 18,23s
- Rm-rf: 8,94s

### Benchmark directo

```

Check notice on line 18 in deletion-speed-benchmark.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

deletion-speed-benchmark.md#L18

Fenced code blocks should have a language specified
❯ mkdir /tmp/empty
❯ time rsync -a --delete "/tmp/empty/" "node_modules2/"
0.87s user 10.95s system 56% cpu 20.922 total

❯ time rm -rf node_modules3
0.31s user 9.63s system 78% cpu 12.619 total

❯ time perl -e 'use File::Path qw(remove_tree); remove_tree("node_modules", {verbose => 0, safe => 0});'

Check notice on line 26 in deletion-speed-benchmark.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

deletion-speed-benchmark.md#L26

Expected: 80; Actual: 104
4.69s user 13.36s system 78% cpu 22.895 total
```

## Directorio pequeño

- Ficheros: 92622
- Tamaño: 1,50GB

### Benchmark directo

Check warning on line 35 in deletion-speed-benchmark.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

deletion-speed-benchmark.md#L35

Multiple headings with the same content

```
❯ time rsync -a --delete "/tmp/empty/" "node_modules/"
0.20s user 3.10s system 95% cpu 3.444 total

❯ time rsync -a --delete --ignore-errors --whole-file --inplace --remove-source-files /tmp/empty/ node_modules/

Check notice on line 41 in deletion-speed-benchmark.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

deletion-speed-benchmark.md#L41

Expected: 80; Actual: 111
0.22s user 3.12s system 95% cpu 3.502 total

❯ time rm -rf node_modules
0.13s user 3.03s system 95% cpu 3.321 total
```
2 changes: 2 additions & 0 deletions src/core/interfaces/file-service.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GetNewestFileResult, RiskAnalysis } from '../interfaces/index.js';
import { ScanOptions } from './folder.interface.js';
import { Observable } from 'rxjs';
import { IsValidRootFolderResult } from './npkill.interface.js';
import { DeletionStrategyManager } from '@core/services/files/index.js';

/**
* Core file system operations service for npkill.
Expand All @@ -11,6 +12,7 @@ import { IsValidRootFolderResult } from './npkill.interface.js';
export interface IFileService {
/** Worker service for handling file operations in background threads. */
fileWorkerService: FileWorkerService;
delStrategyManager: DeletionStrategyManager;

/**
* Calculates the total size of a directory.
Expand Down
16 changes: 13 additions & 3 deletions src/core/npkill.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { FileWorkerService } from './services/files/index.js';
import {
DeletionStrategyManager,
FileWorkerService,
} from './services/files/index.js';
import { from, Observable } from 'rxjs';
import { catchError, filter, map, mergeMap, take, tap } from 'rxjs/operators';
import { ScanStatus } from './interfaces/search-status.model.js';
Expand Down Expand Up @@ -171,14 +174,21 @@ function createDefaultServices(
);
const streamService = new StreamService();
const resultsService = new ResultsService();
const delStrategyManager = new DeletionStrategyManager(actualLogger);

const OSService = OSServiceMap[process.platform];
const OSService = OSServiceMap[
process.platform
] as (typeof OSServiceMap)[keyof typeof OSServiceMap];
if (typeof OSService === 'undefined') {
throw new Error(
`Unsupported platform: ${process.platform}. Cannot load OS service.`,
);
}
const fileService = new OSService(streamService, fileWorkerService);
const fileService = new OSService(
streamService,
fileWorkerService,
delStrategyManager,
);

return {
logger: actualLogger,
Expand Down
8 changes: 7 additions & 1 deletion src/core/services/files/files.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import { readdir, stat } from 'fs/promises';
import { map, Observable, Subject } from 'rxjs';
import { FileWorkerService } from './files.worker.service.js';
import { IsValidRootFolderResult } from '@core/interfaces/npkill.interface.js';
import { DeletionStrategyManager } from './strategies/strategy-manager.js';

export abstract class FileService implements IFileService {
public fileWorkerService: FileWorkerService;
public delStrategyManager: DeletionStrategyManager;

constructor(fileWorkerService: FileWorkerService) {
constructor(
fileWorkerService: FileWorkerService,
delStrategyManager: DeletionStrategyManager,
) {
this.fileWorkerService = fileWorkerService;
this.delStrategyManager = delStrategyManager;
}

abstract deleteDir(path: string): Promise<boolean>;
Expand Down
1 change: 1 addition & 0 deletions src/core/services/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './files.service.js';
export * from './files.worker.service.js';
export * from './unix-files.service.js';
export * from './windows-files.service.js';
export * from './strategies/index.js';
16 changes: 16 additions & 0 deletions src/core/services/files/strategies/deletion-strategy.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface IDeletionStrategy {
readonly name: string;

/**
* Checks if this deletion strategy is available on the current system.
* @returns Promise resolving to true if the strategy can be used.
*/
isAvailable(): Promise<boolean>;

/**
* Deletes a directory using this strategy.
* @param path Path to the directory to delete.
* @returns Promise resolving to true if deletion was successful.
*/
delete(path: string): Promise<boolean>;
}
9 changes: 9 additions & 0 deletions src/core/services/files/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './deletion-strategy.interface.js';
export * from './unix-strategies/rsync-deletion.strategy.js';
export * from './unix-strategies/find-deletion.strategy.js';
export * from './unix-strategies/rm-rf-deletion.strategy.js';
export * from './unix-strategies/perl-deletion.strategy.js';
export * from './windows-strategies/robocopy-deletion.strategy.js';
export * from './windows-strategies/powershell-deletion.strategy.js';
export * from './windows-strategies/node-rm-deletion.strategy.js';
export * from './strategy-manager.js';
105 changes: 105 additions & 0 deletions src/core/services/files/strategies/strategy-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { LoggerService } from '@core/services/logger.service.js';
import { IDeletionStrategy } from './deletion-strategy.interface.js';

/**
* Manages and coordinates different deletion strategies.
*/
export class DeletionStrategyManager {
private availableStrategies: IDeletionStrategy[] = [];
private selectedStrategy: IDeletionStrategy | null = null;
private strategiesInitialized = false;

constructor(private readonly logger: LoggerService) {}

/**
* Registers a deletion strategy.
* Strategies are tested in the order they are registered.
* @param strategy The strategy to register.
*/
registerStrategy(strategy: IDeletionStrategy): void {
this.availableStrategies.push(strategy);
this.logger.info(
`[DeletionStrategyManager] Registered ${strategy.name} strategy.`,
);
}

/**
* Initializes and selects the best available deletion strategy.
* This method tests strategies in registration order and caches the first available one.
* @returns Promise resolving to the selected strategy, or null if none are available.
*/
async initializeStrategy(): Promise<IDeletionStrategy | null> {
if (this.strategiesInitialized && this.selectedStrategy) {
return this.selectedStrategy;
}

for (const strategy of this.availableStrategies) {
try {
const isAvailable = await strategy.isAvailable();
if (isAvailable) {
this.selectedStrategy = strategy;
this.strategiesInitialized = true;
this.logger.info(`[DeletionStrategyManager] Using ${strategy.name}.`);
return strategy;
}
} catch (error) {

Check warning on line 45 in src/core/services/files/strategies/strategy-manager.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/core/services/files/strategies/strategy-manager.ts#L45

'error' is defined but never used.
// Strategy check failed, continue to next one
this.logger.warn(
`[DeletionStrategyManager] ${strategy.name} strategy unavailable.`,
);
}
}

this.strategiesInitialized = true;
return null;
}

/**
* Deletes a directory using the selected strategy.
* Automatically initializes strategy if not already done.
* @param path Path to the directory to delete.
* @returns Promise resolving to true if deletion was successful.
* @throws Error if no strategies are available or deletion fails.
*/
async deleteDirectory(path: string): Promise<boolean> {
if (!this.selectedStrategy) {
await this.initializeStrategy();
}

if (!this.selectedStrategy) {
throw new Error('No deletion strategies are available on this system');
}

try {
return await this.selectedStrategy.delete(path);
} catch (error) {
throw new Error(
`Deletion failed using ${this.selectedStrategy.name} strategy: ${error}`,
);
}
}

/**
* Gets the currently selected strategy.
* @returns The selected strategy or null if none is selected.
*/
getSelectedStrategy(): IDeletionStrategy | null {
return this.selectedStrategy;
}

/**
* Gets all registered strategies.
* @returns Array of all registered strategies.
*/
getAllStrategies(): readonly IDeletionStrategy[] {
return [...this.availableStrategies];
}

/**
* Resets the strategy selection, forcing re-evaluation on next deletion.
*/
resetStrategy(): void {
this.selectedStrategy = null;
this.strategiesInitialized = false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { exec } from 'child_process';
import { IDeletionStrategy } from '../deletion-strategy.interface.js';

export class FindDeletionStrategy implements IDeletionStrategy {
readonly name = 'find';

async isAvailable(): Promise<boolean> {
return new Promise((resolve) => {
exec('command -v find', (error) => {
resolve(error === null);
});
});
}

async delete(path: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const command = `find "${path}" -depth -type f -delete && find "${path}" -depth -type d -delete`;

exec(command, (error, _stdout, stderr) => {
if (error !== null) {
reject(error);
return;
}
if (stderr !== '') {
reject(new Error(stderr));
return;
}
resolve(true);
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { exec } from 'child_process';
import { IDeletionStrategy } from '../deletion-strategy.interface.js';

export class PerlDeletionStrategy implements IDeletionStrategy {
readonly name = 'perl';

async isAvailable(): Promise<boolean> {
return new Promise((resolve) => {
exec('command -v perl', (error) => {
if (error) {
resolve(false);
return;
}

// Check if File::Path module is available
exec('perl -e "use File::Path qw(remove_tree);"', (perlError) => {
resolve(perlError === null);
});
});
});
}

async delete(path: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const perlScript = `use File::Path qw(remove_tree); remove_tree("${path}", {verbose => 0, safe => 0});`;
const command = `perl -e '${perlScript}'`;

exec(command, (error, _stdout, stderr) => {
if (error !== null) {
reject(error);
return;
}
if (stderr !== '') {
reject(new Error(stderr));
return;
}
resolve(true);
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { exec } from 'child_process';
import { IDeletionStrategy } from '../deletion-strategy.interface.js';

export class RmRfDeletionStrategy implements IDeletionStrategy {
readonly name = 'rm-rf';

async isAvailable(): Promise<boolean> {
return new Promise((resolve) => {
exec('command -v rm', (error) => {
resolve(error === null);
});
});
}

async delete(path: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const command = `rm -rf "${path}"`;

exec(command, (error, _stdout, stderr) => {
if (error !== null) {
reject(error);
return;
}
if (stderr !== '') {
reject(new Error(stderr));
return;
}
resolve(true);
});
});
}
}
Loading
Loading