Setup eslint and enforce rules
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-06-02 22:36:25 -05:00
parent 82e7a1f299
commit 1d5056b753
149 changed files with 6104 additions and 3114 deletions

View File

@@ -62,18 +62,18 @@ export class BehaviorSubject<T> {
* True if this subject has been marked complete.
* @type boolean
*/
protected _isComplete: boolean = false
protected subjectIsComplete = false
/**
* The current value of this subject.
*/
protected _value?: T
protected currentValue?: T
/**
* True if any value has been pushed to this subject.
* @type boolean
*/
protected _hasPush: boolean = false
protected hasPush = false
/**
* Register a new subscription to this subject.
@@ -90,7 +90,7 @@ export class BehaviorSubject<T> {
return {
unsubscribe: () => {
this.subscribers = this.subscribers.filter(x => x !== subscriber)
}
},
}
}
@@ -110,9 +110,11 @@ export class BehaviorSubject<T> {
unsubscribe()
},
complete: (val?: T) => {
if ( typeof val !== 'undefined' ) resolve(val)
if ( typeof val !== 'undefined' ) {
resolve(val)
}
unsubscribe()
}
},
})
})
}
@@ -123,9 +125,11 @@ export class BehaviorSubject<T> {
* @return Promise<void>
*/
public async next(val: T): Promise<void> {
if ( this._isComplete ) throw new CompletedObservableError()
this._value = val
this._hasPush = true
if ( this.subjectIsComplete ) {
throw new CompletedObservableError()
}
this.currentValue = val
this.hasPush = true
for ( const subscriber of this.subscribers ) {
if ( subscriber.next ) {
try {
@@ -150,25 +154,32 @@ export class BehaviorSubject<T> {
* @return Promise<void>
*/
public async push(vals: T[]): Promise<void> {
if ( this._isComplete ) throw new CompletedObservableError()
if ( this.subjectIsComplete ) {
throw new CompletedObservableError()
}
await Promise.all(vals.map(val => this.next(val)))
}
/**
* Mark this subject as complete.
* The promise resolves when all subscribers have been pushed to.
* @param [final_val] - optionally, a final value to set
* @param [finalValue] - optionally, a final value to set
* @return Promise<void>
*/
public async complete(final_val?: T): Promise<void> {
if ( this._isComplete ) throw new CompletedObservableError()
if ( typeof final_val === 'undefined' ) final_val = this.value()
else this._value = final_val
public async complete(finalValue?: T): Promise<void> {
if ( this.subjectIsComplete ) {
throw new CompletedObservableError()
}
if ( typeof finalValue === 'undefined' ) {
finalValue = this.value()
} else {
this.currentValue = finalValue
}
for ( const subscriber of this.subscribers ) {
if ( subscriber.complete ) {
try {
await subscriber.complete(final_val)
await subscriber.complete(finalValue)
} catch (e) {
if ( subscriber.error ) {
await subscriber.error(e)
@@ -179,14 +190,14 @@ export class BehaviorSubject<T> {
}
}
this._isComplete = true
this.subjectIsComplete = true
}
/**
* Get the current value of this subject.
*/
public value(): T | undefined {
return this._value
return this.currentValue
}
/**
@@ -194,6 +205,6 @@ export class BehaviorSubject<T> {
* @return boolean
*/
public isComplete(): boolean {
return this._isComplete
return this.subjectIsComplete
}
}

View File

@@ -1,6 +1,6 @@
import {collect} from "../collection/Collection";
import {InvalidJSONStateError, JSONState, Rehydratable} from "./Rehydratable";
import {Pipe} from "./Pipe";
import {collect} from '../collection/Collection'
import {InvalidJSONStateError, JSONState, Rehydratable} from './Rehydratable'
import {Pipe} from './Pipe'
/**
* A class for building and working with messages grouped by keys.
@@ -34,14 +34,14 @@ export class Messages implements Rehydratable {
/** Returns true if the given group has the message. */
public has(key: string, message: string): boolean {
return !!this.messages[key]?.includes(message)
return Boolean(this.messages[key]?.includes(message))
}
/**
* Returns true if any messages are found.
* @param [forKeys] if provided, only search the given keys
*/
public any(forKeys?: string[]) {
public any(forKeys?: string[]): boolean {
if ( forKeys ) {
return forKeys.map(key => this.messages[key])
.filter(Boolean)
@@ -55,8 +55,11 @@ export class Messages implements Rehydratable {
* Returns the first message.
* @param [forKey] if provided, only search the given key
*/
public first(forKey?: string) {
if ( !forKey ) forKey = Object.keys(this.messages)[0]
public first(forKey?: string): string | undefined {
if ( !forKey ) {
forKey = Object.keys(this.messages)[0]
}
if ( forKey && this.messages[forKey].length ) {
return this.messages[forKey][0]
}
@@ -66,20 +69,22 @@ export class Messages implements Rehydratable {
* Return all messages in a flat array.
* @param [forKey] if provided, only search the given key
*/
public all(forKey?: string) {
public all(forKey?: string): string[] {
if ( forKey ) {
return this.messages[forKey] || []
}
return collect<string[]>(Object.values(this.messages)).collapse().all() as string[]
return collect<string[]>(Object.values(this.messages)).collapse()
.all() as string[]
}
/**
* Flat array of only distinct messages.
* @param [forKey] if provided, only search the given key
*/
public unique(forKey?: string) {
return collect<string>(this.all(forKey)).unique<string>().all()
public unique(forKey?: string): string[] {
return collect<string>(this.all(forKey)).unique<string>()
.all()
}
/**
@@ -98,29 +103,31 @@ export class Messages implements Rehydratable {
if ( typeof state === 'object' && !Array.isArray(state) ) {
let all = true
for ( const key in state ) {
if ( !state.hasOwnProperty(key) ) continue;
if ( !Object.prototype.hasOwnProperty.call(state, key) ) {
continue
}
const set = state[key]
if ( !(Array.isArray(set) && set.every(x => typeof x === 'string')) ) {
all = false;
break;
all = false
break
}
}
if ( all ) {
// @ts-ignore
this.messages = state;
this.messages = state as any
}
}
throw new InvalidJSONStateError('Invalid message state object.', { state });
throw new InvalidJSONStateError('Invalid message state object.', { state })
}
toJSON() {
toJSON(): {[key: string]: string[]} {
return this.messages
}
toString() {
return JSON.stringify(this)
toString(): string {
return JSON.stringify(this.toJSON())
}
/**

View File

@@ -45,15 +45,15 @@ export class Pipe<T> {
* Return a new Pipe containing the given subject.
* @param subject
*/
static wrap<subject_t>(subject: subject_t) {
return new Pipe<subject_t>(subject)
static wrap<subjectType>(subject: subjectType): Pipe<subjectType> {
return new Pipe<subjectType>(subject)
}
constructor(
/**
* The item being operated on.
*/
private subject: T
private subject: T,
) {}
/**
@@ -68,7 +68,7 @@ export class Pipe<T> {
*
* @param op
*/
tap<T2>(op: PipeOperator<T, T2>) {
tap<T2>(op: PipeOperator<T, T2>): Pipe<T2> {
return new Pipe(op(this.subject))
}
@@ -79,7 +79,7 @@ export class Pipe<T> {
* @param check
* @param op
*/
when(check: boolean, op: ReflexivePipeOperator<T>) {
when(check: boolean, op: ReflexivePipeOperator<T>): Pipe<T> {
if ( check ) {
return Pipe.wrap(op(this.subject))
}
@@ -94,7 +94,7 @@ export class Pipe<T> {
* @param check
* @param op
*/
unless(check: boolean, op: ReflexivePipeOperator<T>) {
unless(check: boolean, op: ReflexivePipeOperator<T>): Pipe<T> {
return this.when(!check, op)
}
@@ -103,7 +103,7 @@ export class Pipe<T> {
* @param check
* @param op
*/
whenNot(check: boolean, op: ReflexivePipeOperator<T>) {
whenNot(check: boolean, op: ReflexivePipeOperator<T>): Pipe<T> {
return this.unless(check, op)
}

View File

@@ -1,7 +1,7 @@
/**
* Type representing a JSON serializable object.
*/
import {ErrorWithContext} from "../error/ErrorWithContext";
import {ErrorWithContext} from '../error/ErrorWithContext'
export type JSONState = { [key: string]: string | boolean | number | undefined | JSONState | Array<string | boolean | number | undefined | JSONState> }
@@ -15,7 +15,7 @@ export class InvalidJSONStateError extends ErrorWithContext {}
* @param what
* @return boolean
*/
export function isJSONState(what: any): what is JSONState {
export function isJSONState(what: unknown): what is JSONState {
try {
JSON.stringify(what)
return true

View File

@@ -1,20 +1,25 @@
import * as node_uuid from 'uuid'
import {ErrorWithContext} from "../error/ErrorWithContext";
import * as nodeUUID from 'uuid'
import {ErrorWithContext} from '../error/ErrorWithContext'
import {JSONState} from './Rehydratable'
/**
* Make a deep copy of an object.
* @param target
*/
export function deepCopy<T>(target: T): T {
if ( target === null )
if ( target === null ) {
return target
}
if ( target instanceof Date )
if ( target instanceof Date ) {
return new Date(target.getTime()) as any
}
if ( target instanceof Array ) {
const copy = [] as any[]
(target as any[]).forEach(item => { copy.push(item) })
(target as any[]).forEach(item => {
copy.push(item)
})
return copy.map((item: any) => deepCopy<any>(item)) as any
}
@@ -32,28 +37,44 @@ export function deepCopy<T>(target: T): T {
* Given a string of a value, try to infer the JavaScript type.
* @param {string} val
*/
export function infer(val: string) {
if ( !val ) return undefined
else if ( val.toLowerCase() === 'true' ) return true
else if ( val.toLowerCase() === 'false' ) return false
else if ( !isNaN(Number(val)) ) return Number(val)
else if ( isJSON(val) ) return JSON.parse(val)
else if ( val.toLowerCase() === 'null' ) return null
else if ( val.toLowerCase() === 'undefined' ) return undefined
else return val
export function infer(val: string): undefined | boolean | number | JSONState | string | null {
if ( !val ) {
return undefined
} else if ( val.toLowerCase() === 'true' ) {
return true
} else if ( val.toLowerCase() === 'false' ) {
return false
} else if ( !isNaN(Number(val)) ) {
return Number(val)
} else if ( isJSON(val) ) {
return JSON.parse(val)
} else if ( val.toLowerCase() === 'null' ) {
return null
} else if ( val.toLowerCase() === 'undefined' ) {
return undefined
} else {
return val
}
}
/**
* Given an inferred value, try to convert back to the string form.
* @param val
*/
export function uninfer(val: any) {
if ( typeof val === 'undefined' ) return 'undefined'
else if ( val === true ) return 'true'
else if ( val === false ) return 'false'
else if ( !isNaN(Number(val)) ) return `${Number(val)}`
else if ( typeof val === 'string' ) return val
else return JSON.stringify(val)
export function uninfer(val: unknown): string {
if ( typeof val === 'undefined' ) {
return 'undefined'
} else if ( val === true ) {
return 'true'
} else if ( val === false ) {
return 'false'
} else if ( !isNaN(Number(val)) ) {
return `${Number(val)}`
} else if ( typeof val === 'string' ) {
return val
} else {
return JSON.stringify(val)
}
}
/**
@@ -73,8 +94,8 @@ export function isJSON(val: string): boolean {
* Get a universally-unique ID string.
* @return string
*/
export function uuid_v4() {
return node_uuid.v4()
export function uuid4(): string {
return nodeUUID.v4()
}
/**
@@ -87,7 +108,7 @@ export function uuid_v4() {
*/
export function* dataWalkUnsafe<T>(
data: {[key: string]: any} | any[] | undefined,
path: string, defaultValue?: T, currentPath: string = ''
path: string, defaultValue?: T, currentPath = '',
): IterableIterator<[T | T[] | undefined, string]> {
if ( !data ) {
yield [defaultValue, currentPath]
@@ -102,28 +123,35 @@ export function* dataWalkUnsafe<T>(
if ( Array.isArray(data) ) {
if ( part === '*' ) {
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(data, subpath.join('.'), defaultValue, currentPath) ) {
yield val
}
} else {
for ( const key in data ) {
if ( !Object.prototype.hasOwnProperty.call(data, key) ) {
continue
}
yield [data[key], `${currentPath ? currentPath + '.' : ''}${key}`]
}
}
} else if ( !isNaN(parseInt(part)) ) {
const subdata = data[parseInt(part)]
} else if ( !isNaN(parseInt(part, 10)) ) {
const subdata = data[parseInt(part, 10)]
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${parseInt(part)}`) ) {
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${parseInt(part, 10)}`) ) {
yield val
}
} else {
yield [subdata, `${currentPath ? currentPath + '.' : ''}${parseInt(part)}`]
yield [subdata, `${currentPath ? currentPath + '.' : ''}${parseInt(part, 10)}`]
}
} else {
const subdata = data.map(x => x[part])
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${part}`) ) {
yield val
@@ -136,6 +164,7 @@ export function* dataWalkUnsafe<T>(
if ( part === '*' ) {
const subdata = Object.values(data)
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}*`) ) {
yield val
@@ -146,6 +175,7 @@ export function* dataWalkUnsafe<T>(
} else {
const subdata = data[part]
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${part}`) ) {
yield val
@@ -164,28 +194,29 @@ export function* dataWalkUnsafe<T>(
* @param value
* @param data
*/
export function dataSetUnsafe(path: string, value: any, data: {[key: string]: any} | any[] | undefined) {
data = data || ((!isNaN(parseInt(path.split('.')[0]))) ? [] : {})
export function dataSetUnsafe(path: string, value: unknown, data: {[key: string]: any} | any[] | undefined): any {
data = data || ((!isNaN(parseInt(path.split('.')[0], 10))) ? [] : {})
let current = data
const parts = path.split('.')
parts.forEach((part, idx) => {
if ( idx === path.split('.').length - 1 ) {
current[Array.isArray(current) ? parseInt(part) : part] = value
current[Array.isArray(current) ? parseInt(part, 10) : part] = value
return
}
let next = Array.isArray(current) ? parseInt(parts[idx + 1]) : current?.[parts[idx + 1]]
let next = Array.isArray(current) ? parseInt(parts[idx + 1], 10) : current?.[parts[idx + 1]]
if ( !next ) {
next = (!isNaN(parseInt(parts[idx + 1])) ? [] : {})
next = (!isNaN(parseInt(parts[idx + 1], 10)) ? [] : {})
}
if ( Array.isArray(current) ) {
if ( isNaN(parseInt(part)) ) {
throw new ErrorWithContext(`Invalid property name "${part}" of array-type.`, {part, path})
if ( isNaN(parseInt(part, 10)) ) {
throw new ErrorWithContext(`Invalid property name "${part}" of array-type.`, {part,
path})
}
current[parseInt(part)] = next
current[parseInt(part, 10)] = next
current = next
} else {
current[part] = next
@@ -204,69 +235,47 @@ export function dataSetUnsafe(path: string, value: any, data: {[key: string]: an
* @param defaultValue
*/
export function dataGetUnsafe<T>(data: {[key: string]: any} | any[] | undefined, path: string, defaultValue?: T): T | T[] | undefined {
if ( !data ) return
if ( !path ) return data as T
if ( !data ) {
return
}
if ( !path ) {
return data as T
}
const [part, ...subpath] = path.split('.')
if ( Array.isArray(data) ) {
if ( part === '*' ) {
return dataGetUnsafe<T>(data, subpath.join('.'), defaultValue)
} else if ( !isNaN(parseInt(part)) ) {
const subdata = data[parseInt(part)]
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
} else if ( !isNaN(parseInt(part, 10)) ) {
const subdata = data[parseInt(part, 10)]
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
} else {
const subdata = data.map(x => x[part])
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
}
} else {
if ( part === '*' ) {
const subdata = Object.values(data)
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
} else {
const subdata = data[part]
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
}
}
}
function dataGet<
T,
P1 extends keyof NonNullable<T>
>(obj: T, prop1: P1): NonNullable<T>[P1] | undefined;
function dataGet<
T,
P1 extends keyof NonNullable<T>,
P2 extends keyof NonNullable<NonNullable<T>[P1]>
>(obj: T, prop1: P1, prop2: P2): NonNullable<NonNullable<T>[P1]>[P2] | undefined;
function dataGet<
T,
P1 extends keyof NonNullable<T>,
P2 extends keyof NonNullable<NonNullable<T>[P1]>,
P3 extends keyof NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>
>(obj: T, prop1: P1, prop2: P2, prop3: P3): NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>[P3] | undefined;
function dataGet<
T,
P1 extends keyof NonNullable<T>,
P2 extends keyof NonNullable<NonNullable<T>[P1]>,
P3 extends keyof NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>,
P4 extends keyof NonNullable<NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>[P3]>
>(obj: T, prop1: P1, prop2: P2, prop3: P3, prop4: P4): NonNullable<NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>[P3]>[P4] | undefined;
/**
* A type-safe way to get the value of a key nested up to 4 levels deep in an object.
* @param obj
* @param props
*/
function dataGet(obj: any, ...props: string[]): any {
return obj && props.reduce(
(result, prop) => result == null ? undefined : result[prop],
obj
);
}

View File

@@ -1,13 +1,16 @@
export function isDebugging(key: string): boolean {
const env = 'EXTOLLO_DEBUG_' + key.split(/(?:\s|\.)+/).join('_').toUpperCase()
const env = 'EXTOLLO_DEBUG_' + key.split(/(?:\s|\.)+/).join('_')
.toUpperCase()
return process.env[env] === 'yes'
}
export function ifDebugging(key: string, run: () => any) {
if ( isDebugging(key) ) run()
export function ifDebugging(key: string, run: () => any): void {
if ( isDebugging(key) ) {
run()
}
}
export function logIfDebugging(key: string, ...output: any[]) {
ifDebugging(key, () => console.log(`[debug: ${key}]`, ...output))
export function logIfDebugging(key: string, ...output: any[]): void {
ifDebugging(key, () => console.log(`[debug: ${key}]`, ...output)) // eslint-disable-line no-console
}

View File

@@ -1,5 +1,5 @@
import {Collection} from "../collection/Collection";
import {uuid_v4} from "./data";
import {Collection} from '../collection/Collection'
import {uuid4} from './data'
/**
* Type structure for a single item in the global registry.
@@ -12,7 +12,7 @@ export type GlobalRegistrant = { key: string | symbol, value: any }
export class GlobalRegistry extends Collection<GlobalRegistrant> {
constructor() {
super()
this.setGlobal('registry_uuid', uuid_v4())
this.setGlobal('registry_uuid', uuid4())
}
/**
@@ -20,12 +20,15 @@ export class GlobalRegistry extends Collection<GlobalRegistrant> {
* @param key
* @param value
*/
public setGlobal(key: string | symbol, value: any): this {
public setGlobal(key: string | symbol, value: unknown): this {
const existing = this.firstWhere('key', '=', key)
if ( existing ) {
existing.value = value
} else {
this.push({ key, value })
this.push({
key,
value,
})
}
return this

View File

@@ -3,12 +3,13 @@
* @param derivedCtor
* @param {array} baseCtors
*/
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
export function applyMixins(derivedCtor: FunctionConstructor, baseCtors: any[]): void {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
const desc = Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
if ( typeof desc !== 'undefined' )
if ( typeof desc !== 'undefined' ) {
Object.defineProperty(derivedCtor.prototype, name, desc)
}
})
})
}
@@ -16,4 +17,4 @@ export function applyMixins(derivedCtor: any, baseCtors: any[]) {
/**
* Base type for a constructor function.
*/
export type Constructor<T = {}> = new (...args: any[]) => T
export type Constructor<T> = new (...args: any[]) => T

View File

@@ -1,19 +1,10 @@
import * as nodePath from 'path'
import * as fs from 'fs'
import * as mkdirp from 'mkdirp'
import { Filesystem } from "./path/Filesystem"
import { Filesystem } from './path/Filesystem'
import ReadableStream = NodeJS.ReadableStream;
import WritableStream = NodeJS.WritableStream;
/**
* Possible prefixes for files referenced by a UniversalPath.
*/
export enum UniversalPathPrefix {
HTTP = 'http://',
HTTPS = 'https://',
Local = 'file://',
}
/**
* An item that could represent a path.
*/
@@ -24,8 +15,10 @@ export type PathLike = string | UniversalPath
* @param parts
*/
export function universalPath(...parts: PathLike[]): UniversalPath {
let [main, ...concats] = parts
if ( !(main instanceof UniversalPath) ) main = new UniversalPath(main)
let [main, ...concats] = parts // eslint-disable-line prefer-const
if ( !(main instanceof UniversalPath) ) {
main = new UniversalPath(main)
}
return main.concat(...concats)
}
@@ -42,8 +35,11 @@ export function universalPath(...parts: PathLike[]): UniversalPath {
export async function* walk(dir: string): any {
for await (const sub of await fs.promises.opendir(dir) ) {
const entry = nodePath.join(dir, sub.name)
if ( sub.isDirectory() ) yield* walk(entry)
else if ( sub.isFile() ) yield entry
if ( sub.isDirectory() ) {
yield* walk(entry)
} else if ( sub.isFile() ) {
yield entry
}
}
}
@@ -51,8 +47,9 @@ export async function* walk(dir: string): any {
* Class representing some kind of filesystem resource.
*/
export class UniversalPath {
protected _prefix!: string
protected _local!: string
protected resourcePrefix!: string
protected resourceLocalPath!: string
constructor(
/**
@@ -72,15 +69,15 @@ export class UniversalPath {
* Determine the correct prefix for this path.
* @protected
*/
protected setPrefix() {
protected setPrefix(): void {
if ( this.initial.toLowerCase().startsWith('http://') ) {
this._prefix = UniversalPathPrefix.HTTP
this.resourcePrefix = 'http://'
} else if ( this.initial.toLowerCase().startsWith('https://') ) {
this._prefix = UniversalPathPrefix.HTTPS
this.resourcePrefix = 'https://'
} else if ( this.filesystem ) {
this._prefix = this.filesystem.getPrefix()
this.resourcePrefix = this.filesystem.getPrefix()
} else {
this._prefix = UniversalPathPrefix.Local
this.resourcePrefix = 'file://'
}
}
@@ -94,68 +91,68 @@ export class UniversalPath {
*
* @protected
*/
protected setLocal() {
this._local = this.initial
if ( this.initial.toLowerCase().startsWith(this._prefix) ) {
this._local = this._local.slice(this._prefix.length)
protected setLocal(): void {
this.resourceLocalPath = this.initial
if ( this.initial.toLowerCase().startsWith(this.resourcePrefix) ) {
this.resourceLocalPath = this.resourceLocalPath.slice(this.resourcePrefix.length)
}
if ( this._prefix === UniversalPathPrefix.Local && !this._local.startsWith('/') && !this.filesystem ) {
this._local = nodePath.resolve(this._local)
if ( this.resourcePrefix === 'file://' && !this.resourceLocalPath.startsWith('/') && !this.filesystem ) {
this.resourceLocalPath = nodePath.resolve(this.resourceLocalPath)
}
}
/**
* Return a new copy of this UniversalPath instance.
*/
clone() {
clone(): UniversalPath {
return new UniversalPath(this.initial)
}
/**
* Get the UniversalPathPrefix of this resource.
* Get the string of this resource.
*/
get prefix() {
return this._prefix
get prefix(): string {
return this.resourcePrefix
}
/**
* Returns true if this resource refers to a file on the local filesystem.
*/
get isLocal() {
return this._prefix === UniversalPathPrefix.Local && !this.filesystem
get isLocal(): boolean {
return this.resourcePrefix === 'file://' && !this.filesystem
}
/**
* Returns true if this resource refers to a file on a remote filesystem.
*/
get isRemote() {
return this._prefix !== UniversalPathPrefix.Local || this.filesystem
get isRemote(): boolean {
return Boolean(this.resourcePrefix !== 'file://' || this.filesystem)
}
/**
* Get the non-prefixed path to this resource.
*/
get unqualified() {
return this._local
get unqualified(): string {
return this.resourceLocalPath
}
/**
* Get the path to this resource as it would be accessed from the current filesystem.
*/
get toLocal() {
get toLocal(): string {
if ( this.isLocal ) {
return this._local
return this.resourceLocalPath
} else {
return `${this.prefix}${this._local}`
return `${this.prefix}${this.resourceLocalPath}`
}
}
/**
* Get the fully-prefixed path to this resource.
*/
get toRemote() {
return `${this.prefix}${this._local}`
get toRemote(): string {
return `${this.prefix}${this.resourceLocalPath}`
}
/**
@@ -183,15 +180,15 @@ export class UniversalPath {
* @param path
*/
public append(path: PathLike): this {
this._local += String(path)
this.resourceLocalPath += String(path)
return this
}
/**
* Cast the path to a string (fully-prefixed).
*/
toString() {
return `${this.prefix}${this._local}`
toString(): string {
return `${this.prefix}${this.resourceLocalPath}`
}
/**
@@ -204,8 +201,8 @@ export class UniversalPath {
* myFile.ext // => 'txt'
* ```
*/
get ext() {
return nodePath.extname(this._local)
get ext(): string {
return nodePath.extname(this.resourceLocalPath)
}
/**
@@ -223,26 +220,26 @@ export class UniversalPath {
* }
* ```
*/
walk() {
return walk(this._local)
walk(): any {
return walk(this.resourceLocalPath)
}
/**
* Returns true if the given resource exists at the path.
*/
async exists() {
async exists(): Promise<boolean> {
if ( this.filesystem ) {
const stat = await this.filesystem.stat({
storePath: this._local
storePath: this.resourceLocalPath,
})
return stat.exists
}
try {
await fs.promises.stat(this._local)
await fs.promises.stat(this.resourceLocalPath)
return true
} catch(e) {
} catch (e) {
return false
}
}
@@ -250,13 +247,13 @@ export class UniversalPath {
/**
* Recursively create this path as a directory. Equivalent to `mkdir -p` on Linux.
*/
async mkdir() {
async mkdir(): Promise<void> {
if ( this.filesystem ) {
await this.filesystem.mkdir({
storePath: this._local
storePath: this.resourceLocalPath,
})
} else {
await mkdirp(this._local)
await mkdirp(this.resourceLocalPath)
}
}
@@ -264,22 +261,27 @@ export class UniversalPath {
* Write the given data to this resource as a file.
* @param data
*/
async write(data: string | Buffer) {
if ( typeof data === 'string' ) data = Buffer.from(data, 'utf8')
async write(data: string | Buffer): Promise<void> {
if ( typeof data === 'string' ) {
data = Buffer.from(data, 'utf8')
}
if ( this.filesystem ) {
const stream = await this.filesystem.putStoreFileAsStream({
storePath: this._local
storePath: this.resourceLocalPath,
})
await new Promise<void>((res, rej) => {
stream.write(data, err => {
if ( err ) rej(err)
else res()
if ( err ) {
rej(err)
} else {
res()
}
})
})
} else {
const fd = await fs.promises.open(this._local, 'w')
const fd = await fs.promises.open(this.resourceLocalPath, 'w')
await fd.write(data)
await fd.close()
}
@@ -291,24 +293,24 @@ export class UniversalPath {
async writeStream(): Promise<WritableStream> {
if ( this.filesystem ) {
return this.filesystem.putStoreFileAsStream({
storePath: this._local
storePath: this.resourceLocalPath,
})
} else {
return fs.createWriteStream(this._local)
return fs.createWriteStream(this.resourceLocalPath)
}
}
/**
* Read the data from this resource's file as a string.
*/
async read() {
async read(): Promise<string> {
let stream: ReadableStream
if ( this.filesystem ) {
stream = await this.filesystem.getStoreFileAsStream({
storePath: this._local
storePath: this.resourceLocalPath,
})
} else {
stream = fs.createReadStream(this._local)
stream = fs.createReadStream(this.resourceLocalPath)
}
const chunks: any[] = []
@@ -325,14 +327,14 @@ export class UniversalPath {
async readStream(): Promise<ReadableStream> {
if ( this.filesystem ) {
return this.filesystem.getStoreFileAsStream({
storePath: this._local
storePath: this.resourceLocalPath,
})
} else {
return fs.createReadStream(this._local)
return fs.createReadStream(this.resourceLocalPath)
}
}
/*get mime_type() {
/* get mime_type() {
return Mime.lookup(this.ext)
}

View File

@@ -1,10 +1,10 @@
import {UniversalPath} from "../path"
import * as path from "path"
import * as os from "os"
import {uuid_v4} from "../data"
import {UniversalPath} from '../path'
import * as path from 'path'
import * as os from 'os'
import {uuid4} from '../data'
import ReadableStream = NodeJS.ReadableStream;
import WritableStream = NodeJS.WritableStream;
import {ErrorWithContext} from "../../error/ErrorWithContext";
import {ErrorWithContext} from '../../error/ErrorWithContext'
/**
* Error thrown when an operation is attempted on a non-existent file.
@@ -77,12 +77,12 @@ export abstract class Filesystem {
/**
* Called when the Filesystem driver is initialized. Do any standup here.
*/
public open(): void | Promise<void> {}
public open(): void | Promise<void> {} // eslint-disable-line @typescript-eslint/no-empty-function
/**
* Called when the Filesystem driver is destroyed. Do any cleanup here.
*/
public close(): void | Promise<void> {}
public close(): void | Promise<void> {} // eslint-disable-line @typescript-eslint/no-empty-function
/**
* Get the URI prefix for this filesystem.
@@ -184,9 +184,13 @@ export abstract class Filesystem {
* @param tags
* @protected
*/
protected _normalizeTags(tag?: string, tags?: string[]): string[] {
if ( !tags ) tags = []
if ( tag ) tags.push(tag)
protected normalizeTags(tag?: string, tags?: string[]): string[] {
if ( !tags ) {
tags = []
}
if ( tag ) {
tags.push(tag)
}
return tags
}
@@ -195,6 +199,6 @@ export abstract class Filesystem {
* @protected
*/
protected tempName(): string {
return path.resolve(os.tmpdir(), uuid_v4())
return path.resolve(os.tmpdir(), uuid4())
}
}

View File

@@ -1,9 +1,9 @@
import {FileMetadata, FileNotFoundError, Filesystem, Stat} from "./Filesystem"
import * as fs from "fs"
import * as path from "path"
import {UniversalPath} from "../path"
import * as rimraf from "rimraf"
import * as mkdirp from "mkdirp"
import {FileMetadata, FileNotFoundError, Filesystem, Stat} from './Filesystem'
import * as fs from 'fs'
import * as path from 'path'
import {UniversalPath} from '../path'
import * as rimraf from 'rimraf'
import * as mkdirp from 'mkdirp'
export interface LocalFilesystemConfig {
baseDir: string
@@ -15,8 +15,10 @@ export interface LocalFilesystemConfig {
*/
export class LocalFilesystem extends Filesystem {
constructor(
protected readonly baseConfig: LocalFilesystemConfig
) { super() }
protected readonly baseConfig: LocalFilesystemConfig,
) {
super()
}
async open(): Promise<void> {
// Make sure the base directory exists
@@ -26,14 +28,14 @@ export class LocalFilesystem extends Filesystem {
}
public getPrefix(): string {
return 'local://'
return 'file://'
}
public async putLocalFile({localPath, storePath, ...args}: {localPath: string, storePath: string, mimeType?: string, tags?: string[], tag?: string}) {
public async putLocalFile({localPath, storePath, ...args}: {localPath: string, storePath: string, mimeType?: string, tags?: string[], tag?: string}): Promise<void> {
await fs.promises.copyFile(localPath, this.storePath(storePath))
await fs.promises.writeFile(this.metadataPath(storePath), JSON.stringify({
mimeType: args.mimeType,
tags: this._normalizeTags(args.tag, args.tags),
tags: this.normalizeTags(args.tag, args.tags),
}))
}
@@ -51,13 +53,13 @@ export class LocalFilesystem extends Filesystem {
return fs.createWriteStream(this.storePath(args.storePath))
}
public async getMetadata(storePath: string) {
public async getMetadata(storePath: string): Promise<FileMetadata> {
try {
const json = (await fs.promises.readFile(this.metadataPath(storePath))).toString('utf-8')
return JSON.parse(json)
} catch (e) {
return {
tags: []
tags: [],
}
}
}
@@ -81,7 +83,7 @@ export class LocalFilesystem extends Filesystem {
exists: true,
sizeInBytes: stat.size,
mimeType: meta.mimeType,
tags: meta.tags,
tags: meta.tags ?? [],
accessed: stat.atime,
modified: stat.mtime,
created: stat.ctime,
@@ -92,7 +94,7 @@ export class LocalFilesystem extends Filesystem {
path: new UniversalPath(args.storePath, this),
exists: false,
sizeInBytes: 0,
tags: []
tags: [],
}
}
@@ -107,9 +109,13 @@ export class LocalFilesystem extends Filesystem {
fs.utimes(storePath, time, time, err => {
if ( err ) {
fs.open(storePath, 'w', (err2, fd) => {
if ( err2 ) return rej(err2)
if ( err2 ) {
return rej(err2)
}
fs.close(fd, err3 => {
if ( err3 ) return rej(err3)
if ( err3 ) {
return rej(err3)
}
res()
})
})
@@ -127,9 +133,11 @@ export class LocalFilesystem extends Filesystem {
} else {
await new Promise<void>((res, rej) => {
rimraf(this.storePath(args.storePath), err => {
if ( err ) return rej(err)
else {
fs.promises.unlink(this.metadataPath(args.storePath)).then(() => res()).catch(rej)
if ( err ) {
return rej(err)
} else {
fs.promises.unlink(this.metadataPath(args.storePath)).then(() => res())
.catch(rej)
}
})
})

View File

@@ -1,19 +1,21 @@
import {FileMetadata, Filesystem, Stat} from "./Filesystem"
import * as ssh2 from "ssh2"
import * as path from "path"
import * as fs from "fs"
import {FileMetadata, Filesystem, Stat} from './Filesystem'
import * as ssh2 from 'ssh2'
import * as path from 'path'
import * as fs from 'fs'
import ReadableStream = NodeJS.ReadableStream
import {UniversalPath} from "../path"
import {UniversalPath} from '../path'
/**
* A Filesystem implementation that stores files on remote hosts via SFTP/SSH.
*/
export class SSHFilesystem extends Filesystem {
private _ssh?: ssh2.Client
private sshClient?: ssh2.Client
constructor(
protected readonly baseConfig: { ssh: ssh2.ConnectConfig, baseDir: string },
) { super() }
) {
super()
}
getPrefix(): string {
return `sftp+${this.baseConfig.ssh.host}://`
@@ -48,7 +50,7 @@ export class SSHFilesystem extends Filesystem {
const sftp = await this.getSFTP()
await sftp.writeFile(this.metadataPath(args.storePath), JSON.stringify({
mimeType: args.mimeType,
tags: this._normalizeTags(args.tag, args.tags)
tags: this.normalizeTags(args.tag, args.tags),
}))
// pipe the local file to the store
@@ -69,8 +71,11 @@ export class SSHFilesystem extends Filesystem {
const sftp = await this.getSFTP()
await new Promise<void>((res, rej) => {
sftp.mkdir(this.storePath(args.storePath), err => {
if ( err ) rej(err)
else res()
if ( err ) {
rej(err)
} else {
res()
}
})
})
}
@@ -80,11 +85,15 @@ export class SSHFilesystem extends Filesystem {
await new Promise<void>((res, rej) => {
sftp.unlink(this.storePath(args.storePath), err => {
if ( err ) return rej(err)
else {
if ( err ) {
return rej(err)
} else {
sftp.unlink(this.metadataPath(args.storePath), err2 => {
if ( err2 ) rej(err2)
else res()
if ( err2 ) {
rej(err2)
} else {
res()
}
})
}
})
@@ -96,9 +105,11 @@ export class SSHFilesystem extends Filesystem {
try {
const stat = await new Promise<any>((res, rej) => {
sftp.stat(this.storePath(args.storePath), (err, stat) => {
if ( err ) return rej(err)
res(stat)
sftp.stat(this.storePath(args.storePath), (err, sftpStats) => {
if ( err ) {
return rej(err)
}
res(sftpStats)
})
})
@@ -121,7 +132,7 @@ export class SSHFilesystem extends Filesystem {
path: new UniversalPath(args.storePath, this),
exists: false,
sizeInBytes: 0,
tags: []
tags: [],
}
}
}
@@ -135,9 +146,13 @@ export class SSHFilesystem extends Filesystem {
sftp.utimes(storePath, time, time, err => {
if ( err ) {
sftp.open(storePath, 'w', (err2, fd) => {
if ( err2 ) return rej(err2)
if ( err2 ) {
return rej(err2)
}
sftp.close(fd, err3 => {
if ( err3 ) return rej(err3)
if ( err3 ) {
return rej(err3)
}
res()
})
})
@@ -153,13 +168,15 @@ export class SSHFilesystem extends Filesystem {
const sftp = await this.getSFTP()
return new Promise((res, rej) => {
sftp.readFile(this.metadataPath(storePath), (err, buffer) => {
if ( err ) rej(err)
if ( err ) {
rej(err)
}
res(JSON.parse(buffer.toString('utf-8')))
})
})
} catch (e) {
return {
tags: []
tags: [],
}
}
}
@@ -169,14 +186,17 @@ export class SSHFilesystem extends Filesystem {
const metaPath = this.metadataPath(storePath)
await new Promise<void>((res, rej) => {
sftp.writeFile(metaPath, JSON.stringify(meta), err => {
if ( err ) rej(err)
else res()
if ( err ) {
rej(err)
} else {
res()
}
})
})
}
async close(): Promise<void> {
await this._ssh?.end()
await this.sshClient?.end()
}
/**
@@ -184,13 +204,15 @@ export class SSHFilesystem extends Filesystem {
* If a connection already exists, re-use it.
*/
async getSSH(): Promise<ssh2.Client> {
if ( this._ssh ) return this._ssh
if ( this.sshClient ) {
return this.sshClient
}
return new Promise((res, rej) => {
const client = new ssh2.Client()
client.on('ready', () => {
this._ssh = client
this.sshClient = client
res(client)
}).connect(this.baseConfig.ssh)
@@ -206,8 +228,11 @@ export class SSHFilesystem extends Filesystem {
return new Promise((res, rej) => {
ssh.sftp((err, sftp) => {
if ( err ) rej(err)
else res(sftp)
if ( err ) {
rej(err)
} else {
res(sftp)
}
})
})
}
@@ -236,7 +261,7 @@ export class SSHFilesystem extends Filesystem {
* @protected
*/
protected streamToString(stream: NodeJS.ReadableStream): Promise<string> {
const chunks: Buffer[] = [];
const chunks: Buffer[] = []
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
stream.on('error', (err) => reject(err))

View File

@@ -5,7 +5,7 @@
* @param length
* @param padWith
*/
export function padRight(string: string, length: number, padWith: string = ' ') {
export function padRight(string: string, length: number, padWith = ' '): string {
while ( string.length < length ) {
string += padWith
}
@@ -20,7 +20,7 @@ export function padRight(string: string, length: number, padWith: string = ' ')
* @param length
* @param padWith
*/
export function padLeft(string: string, length: number, padWith: string = ' ') {
export function padLeft(string: string, length: number, padWith = ' '): string {
while ( string.length < length ) {
string = `${padWith}${string}`
}
@@ -35,8 +35,8 @@ export function padLeft(string: string, length: number, padWith: string = ' ') {
* @param length
* @param padWith
*/
export function padCenter(string: string, length: number, padWith: string = ' ') {
let bit = false
export function padCenter(string: string, length: number, padWith = ' '): string {
const bit = false
while ( string.length < length ) {
if ( bit ) {
string = `${padWith}${string}`

View File

@@ -31,10 +31,10 @@ export interface TimeoutSubscriber<T> {
* @param {number} timeout - timeout in milliseconds
* @param {Promise} promise - the promise to subscribe to
*/
export function withTimeout<T>(timeout: number, promise: Promise<T>) {
let onTimeHandler: (arg: T) => any = (arg) => {}
let lateHandler: (arg: T) => any = (arg) => {}
let timeoutHandler: () => any = () => {}
export function withTimeout<T>(timeout: number, promise: Promise<T>): TimeoutSubscriber<T> {
let onTimeHandler: (arg: T) => any = () => {} // eslint-disable-line @typescript-eslint/no-empty-function
let lateHandler: (arg: T) => any = () => {} // eslint-disable-line @typescript-eslint/no-empty-function
let timeoutHandler: () => any = () => {} // eslint-disable-line @typescript-eslint/no-empty-function
const sub = {
onTime: handler => {
@@ -54,7 +54,9 @@ export function withTimeout<T>(timeout: number, promise: Promise<T>) {
let resolved = false
setTimeout(() => {
expired = true
if ( !resolved ) timeoutHandler()
if ( !resolved ) {
timeoutHandler()
}
}, timeout)
const result: T = await promise
@@ -67,7 +69,7 @@ export function withTimeout<T>(timeout: number, promise: Promise<T>) {
}
return result
}
},
} as TimeoutSubscriber<T>
return sub