Skip to content

Commit

Permalink
Feat/node polyfill (#109)
Browse files Browse the repository at this point in the history
* refactor(windows-remove-dir.strategy): remove unused code

* feat(src): add strategies

* refactor(recursive-rmdir-node-support.constants): change constants to string

* refactor(windows-files.service): add strategy selection

* fix(windows-files.service): instantiate windows strategy manager

* refactor(recursive-rmdir-node-support.constants): string to object

* fix(windows-remove-dir.strategy): add missing bind in selectStrategy

* refactor(polyfill): strategy + CoR

* Update nodejs.yml

* fix(strategies): swap node 12 and 14 strategies

* feat(package.json): add --verbose to jest

Co-authored-by: nyagarcia <nyablk97@gmail.com>
  • Loading branch information
Caballerog and NyaGarcia committed Aug 27, 2021
1 parent e8180f7 commit 92dd94c
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 132 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"build": "gulp",
"build-go-bin": "gulp buildGo",
"start": "ts-node -r tsconfig-paths/register ./src/main.ts",
"test": "jest",
"test": "jest --verbose",
"test:watch": "jest --watch",
"test:mutant": "stryker run",
"release": "npm run build && np",
Expand Down
13 changes: 6 additions & 7 deletions src/constants/recursive-rmdir-node-support.constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { INodeVersion } from '@interfaces/node-version.interface'
export const RECURSIVE_RMDIR_NODE_VERSION_SUPPORT = { major: 12, minor: 10 };
export const RM_NODE_VERSION_SUPPORT = { major: 14, minor: 14 };

export const RECURSIVE_RMDIR_NODE_VERSION_SUPPORT: Pick<INodeVersion, 'major' | 'minor'> = {
major: 12,
minor: 10
};

export const RECURSIVE_RMDIR_IGNORED_ERROR_CODES: string[] = ['ENOTEMPTY', 'EEXIST'];
export const RECURSIVE_RMDIR_IGNORED_ERROR_CODES: string[] = [
'ENOTEMPTY',
'EEXIST',
];
131 changes: 7 additions & 124 deletions src/services/windows-files.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { normalize, join as pathJoin } from 'path';
import { rm, lstat, unlink, readdir } from 'fs';
import { version } from 'process';

import * as getSize from 'get-folder-size';

import { FileService, StreamService } from '@core/services';
import { IErrorCallback, INodeVersion } from '@core/interfaces';
import { RECURSIVE_RMDIR_IGNORED_ERROR_CODES, RECURSIVE_RMDIR_NODE_VERSION_SUPPORT } from '@core/constants';

import { IListDirParams } from '@core/interfaces/list-dir-params.interface';
import { Observable } from 'rxjs';
import { WindowsStrategyManager } from '@core/strategies/windows-remove-dir.strategy';
import { normalize } from 'path';
import { spawn } from 'child_process';

export class WindowsFilesService extends FileService {
private windowsStrategyManager: WindowsStrategyManager =
new WindowsStrategyManager();

constructor(private streamService: StreamService) {
super();
}

getFolderSize(path: string): Observable<number> {
return new Observable(observer => {
return new Observable((observer) => {
getSize(path, (err, size) => {
if (err) {
throw err;
Expand All @@ -42,122 +41,6 @@ export class WindowsFilesService extends FileService {
}

deleteDir(path: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const { major: supportedMajor, minor: supportedMinor } = RECURSIVE_RMDIR_NODE_VERSION_SUPPORT;

if (this.nodeVersion instanceof Error) {
return reject(this.nodeVersion);
}

const { major: currentMajor, minor: currentMinor } = this.nodeVersion;

if (currentMajor < supportedMajor || (currentMajor === supportedMajor && currentMinor < supportedMinor)) {
this.removeRecursively(path, err => {
if (err) {
return reject(err);
}
resolve(true);
});
} else {
rm(path, { recursive: true }, err => {
if (err) {
reject(err);
}
resolve(true);
});
}
});
}

protected get nodeVersion(): INodeVersion | Error {
const releaseVersionsRegExp: RegExp = /^v(\d{1,2})\.(\d{1,2})\.(\d{1,2})/;
const versionMatch = version.match(releaseVersionsRegExp);

if (!versionMatch) {
return new Error(`Unable to parse Node version: ${version}`);
}

return {
major: parseInt(versionMatch[1], 10),
minor: parseInt(versionMatch[2], 10),
patch: parseInt(versionMatch[3], 10),
};
}

protected removeRecursively(dirOrFilePath: string, callback: IErrorCallback): void {
lstat(dirOrFilePath, (lstatError, stats) => {
// No such file or directory - Done
if (lstatError && lstatError.code === 'ENOENT') {
return callback(null);
}

if(stats.isDirectory()) {
return this.removeDirectory(dirOrFilePath, callback);
}

unlink(dirOrFilePath, rmError => {
// No such file or directory - Done
if (rmError && rmError.code === 'ENOENT') {
return callback(null);
}

if (rmError && rmError.code === 'EISDIR') {
return this.removeDirectory(dirOrFilePath, callback);
}

callback(rmError);
});
});
}

protected removeDirectory(path: string, callback) {
rm(path, rmDirError => {
// We ignore certain error codes
// in order to simulate 'recursive' mode
if (rmDirError && RECURSIVE_RMDIR_IGNORED_ERROR_CODES.includes(rmDirError.code)) {
return this.removeChildren(path, callback);
}

callback(rmDirError);
});
}

protected removeChildren(path: string, callback) {
readdir(path, (readdirError, ls) => {
if (readdirError) {
return callback(readdirError);
}

let contentInDirectory = ls.length;
let done = false;

// removeDirectory only allows deleting directories
// that has no content inside (empty directory).
if (!contentInDirectory) {
return rm(path, callback);
}

ls.forEach(dirOrFile => {
const dirOrFilePath = pathJoin(path, dirOrFile);

this.removeRecursively(dirOrFilePath, error => {
if (done) {
return;
}

if (error) {
done = true;
return callback(error);
}

contentInDirectory--;
// No more content inside.
// Remove the directory.
if (!contentInDirectory) {
rm(path, callback);
}
});
});
});
return this.windowsStrategyManager.deleteDir(path);
}
}
3 changes: 3 additions & 0 deletions src/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './windows-default.strategy';
export * from './windows-node12.strategy';
export * from './windows-node14.strategy';
91 changes: 91 additions & 0 deletions src/strategies/windows-default.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NoParamCallback, lstat, readdir, rmdir, unlink } from 'fs';

import { RECURSIVE_RMDIR_IGNORED_ERROR_CODES } from '@core/constants';
import { WindowsStrategy } from './windows-strategy.abstract';
import { join as pathJoin } from 'path';

export class WindowsDefaultStrategy extends WindowsStrategy {
remove(dirOrFilePath: string, callback: NoParamCallback): boolean {
lstat(dirOrFilePath, (lstatError, stats) => {
// No such file or directory - Done
if (lstatError && lstatError.code === 'ENOENT') {
return callback(null);
}

if (stats.isDirectory()) {
return this.removeDirectory(dirOrFilePath, callback);
}

unlink(dirOrFilePath, (rmError) => {
// No such file or directory - Done
if (rmError && rmError.code === 'ENOENT') {
return callback(null);
}

if (rmError && rmError.code === 'EISDIR') {
return this.removeDirectory(dirOrFilePath, callback);
}

callback(rmError);
});
});
return true;
}

isSupported(): boolean {
return true;
}

private removeDirectory(path: string, callback) {
rmdir(path, (rmDirError) => {
// We ignore certain error codes
// in order to simulate 'recursive' mode
if (
rmDirError &&
RECURSIVE_RMDIR_IGNORED_ERROR_CODES.includes(rmDirError.code)
) {
return this.removeChildren(path, callback);
}

callback(rmDirError);
});
}
private removeChildren(path: string, callback) {
readdir(path, (readdirError, ls) => {
if (readdirError) {
return callback(readdirError);
}

let contentInDirectory = ls.length;
let done = false;

// removeDirectory only allows deleting directories
// that has no content inside (empty directory).
if (!contentInDirectory) {
return rmdir(path, callback);
}

ls.forEach((dirOrFile) => {
const dirOrFilePath = pathJoin(path, dirOrFile);

this.remove(dirOrFilePath, (error) => {
if (done) {
return;
}

if (error) {
done = true;
return callback(error);
}

contentInDirectory--;
// No more content inside.
// Remove the directory.
if (!contentInDirectory) {
rmdir(path, callback);
}
});
});
});
}
}
22 changes: 22 additions & 0 deletions src/strategies/windows-node12.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NoParamCallback, rmdir } from 'fs';

import { RECURSIVE_RMDIR_NODE_VERSION_SUPPORT } from '@core/constants';
import { WindowsStrategy } from './windows-strategy.abstract';

export class WindowsNode12Strategy extends WindowsStrategy {
remove(path: string, callback: NoParamCallback): boolean {
if (this.isSupported()) {
rmdir(path, { recursive: true }, callback);
return true;
}
return this.checkNext(path, callback);
}

isSupported(): boolean {
return (
this.major > RECURSIVE_RMDIR_NODE_VERSION_SUPPORT.major ||
(this.major === RECURSIVE_RMDIR_NODE_VERSION_SUPPORT.major &&
this.minor > RECURSIVE_RMDIR_NODE_VERSION_SUPPORT.minor)
);
}
}
22 changes: 22 additions & 0 deletions src/strategies/windows-node14.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NoParamCallback, rm } from 'fs';

import { RM_NODE_VERSION_SUPPORT } from '@core/constants/recursive-rmdir-node-support.constants';
import { WindowsStrategy } from './windows-strategy.abstract';

export class WindowsNode14Strategy extends WindowsStrategy {
remove(path: string, callback: NoParamCallback): boolean {
if (this.isSupported()) {
rm(path, { recursive: true }, callback);
return true;
}
return this.checkNext(path, callback);
}

isSupported(): boolean {
return (
this.major > RM_NODE_VERSION_SUPPORT.major ||
(this.major === RM_NODE_VERSION_SUPPORT.major &&
this.minor > RM_NODE_VERSION_SUPPORT.minor)
);
}
}
24 changes: 24 additions & 0 deletions src/strategies/windows-remove-dir.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
WindowsNode12Strategy,
WindowsNode14Strategy,
WindowsDefaultStrategy,
} from '.';
import { WindowsStrategy } from './windows-strategy.abstract';

export class WindowsStrategyManager {
deleteDir(path: string): Promise<boolean> {
const windowsStrategy: WindowsStrategy = new WindowsNode14Strategy();
windowsStrategy
.setNextStrategy(new WindowsNode12Strategy())
.setNextStrategy(new WindowsDefaultStrategy());

return new Promise((resolve, reject) => {
windowsStrategy.remove(path, (err) => {
if (err) {
return reject(err);
}
resolve(true);
});
});
}
}
44 changes: 44 additions & 0 deletions src/strategies/windows-strategy.abstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { INodeVersion } from '@core/interfaces';
import { NoParamCallback } from 'fs';
import { version } from 'process';

export abstract class WindowsStrategy {
private next: WindowsStrategy;
protected major: number;
protected minor: number;

abstract remove(path: string, callback: NoParamCallback): boolean;
abstract isSupported(major: number, minor: number): boolean;

constructor() {
const { major, minor } = this.getNodeVersion();
this.major = major;
this.minor = minor;
}
public setNextStrategy(next: WindowsStrategy): WindowsStrategy {
this.next = next;
return next;
}

protected checkNext(path: string, callback): boolean {
if (!this.next) {
return true;
}
return this.next.remove(path, callback);
}

private getNodeVersion(): INodeVersion {
const releaseVersionsRegExp: RegExp = /^v(\d{1,2})\.(\d{1,2})\.(\d{1,2})/;
const versionMatch = version.match(releaseVersionsRegExp);

if (!versionMatch) {
throw new Error(`Unable to parse Node version: ${version}`);
}

return {
major: parseInt(versionMatch[1], 10),
minor: parseInt(versionMatch[2], 10),
patch: parseInt(versionMatch[3], 10),
};
}
}

0 comments on commit 92dd94c

Please sign in to comment.