Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/node polyfill #109

Merged
merged 16 commits into from
Aug 27, 2021
Merged
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
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),
};
}
}