2021-06-02 01:59:40 +00:00
|
|
|
import * as nodePath from 'path'
|
|
|
|
import * as fs from 'fs'
|
|
|
|
import * as mkdirp from 'mkdirp'
|
2021-07-08 01:13:23 +00:00
|
|
|
import * as mime from 'mime-types'
|
|
|
|
import {FileNotFoundError, Filesystem} from './path/Filesystem'
|
|
|
|
import {Collection} from '../collection/Collection'
|
|
|
|
import {Readable, Writable} from 'stream'
|
2022-01-17 21:57:40 +00:00
|
|
|
import {Pipeline} from './Pipe'
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An item that could represent a path.
|
|
|
|
*/
|
|
|
|
export type PathLike = string | UniversalPath
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new UniversalPath from the given path-like segments.
|
|
|
|
* @param parts
|
|
|
|
*/
|
|
|
|
export function universalPath(...parts: PathLike[]): UniversalPath {
|
2021-06-03 03:36:25 +00:00
|
|
|
let [main, ...concats] = parts // eslint-disable-line prefer-const
|
|
|
|
if ( !(main instanceof UniversalPath) ) {
|
|
|
|
main = new UniversalPath(main)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
return main.concat(...concats)
|
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* Format bytes as human-readable text.
|
|
|
|
*
|
|
|
|
* @param bytes Number of bytes.
|
|
|
|
* @param si True to use metric (SI) units, aka powers of 1000. False to use
|
|
|
|
* binary (IEC), aka powers of 1024.
|
|
|
|
* @param dp Number of decimal places to display.
|
|
|
|
* @see https://stackoverflow.com/a/14919494/4971138
|
|
|
|
*/
|
|
|
|
export function bytesToHumanFileSize(bytes: number, si = false, dp = 1): string {
|
|
|
|
const thresh = si ? 1000 : 1024
|
|
|
|
|
|
|
|
if (Math.abs(bytes) < thresh) {
|
|
|
|
return bytes + ' B'
|
|
|
|
}
|
|
|
|
|
|
|
|
const units = si
|
|
|
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
|
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
|
|
|
let u = -1
|
|
|
|
const r = 10 ** dp
|
|
|
|
|
|
|
|
do {
|
|
|
|
bytes /= thresh
|
|
|
|
++u
|
|
|
|
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1)
|
|
|
|
|
|
|
|
return bytes.toFixed(dp) + ' ' + units[u]
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Walk recursively over entries in a directory.
|
|
|
|
*
|
|
|
|
* Right now the types are kinda weird for async iterables. This is like an async
|
|
|
|
* IterableIterable that resolves a string or another IterableIterable of the same type.
|
|
|
|
*
|
|
|
|
* Hence why it's separate from the UniversalPath class.
|
|
|
|
*
|
|
|
|
* @param dir
|
|
|
|
*/
|
|
|
|
export async function* walk(dir: string): any {
|
|
|
|
for await (const sub of await fs.promises.opendir(dir) ) {
|
|
|
|
const entry = nodePath.join(dir, sub.name)
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( sub.isDirectory() ) {
|
|
|
|
yield* walk(entry)
|
|
|
|
} else if ( sub.isFile() ) {
|
|
|
|
yield entry
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class representing some kind of filesystem resource.
|
|
|
|
*/
|
|
|
|
export class UniversalPath {
|
2021-06-03 03:36:25 +00:00
|
|
|
protected resourcePrefix!: string
|
|
|
|
|
|
|
|
protected resourceLocalPath!: string
|
2021-06-02 01:59:40 +00:00
|
|
|
|
2021-10-18 17:48:16 +00:00
|
|
|
protected resourceQuery: URLSearchParams = new URLSearchParams()
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
constructor(
|
|
|
|
/**
|
|
|
|
* The path string this path refers to.
|
|
|
|
*
|
|
|
|
* @example /home/user/file.txt
|
|
|
|
* @example https://site.com/file.txt
|
|
|
|
*/
|
|
|
|
protected readonly initial: string,
|
|
|
|
protected readonly filesystem?: Filesystem,
|
|
|
|
) {
|
|
|
|
this.setPrefix()
|
|
|
|
this.setLocal()
|
2021-10-18 17:48:16 +00:00
|
|
|
|
|
|
|
if ( this.isRemote ) {
|
|
|
|
this.resourceQuery = (new URL(this.toRemote)).searchParams
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine the correct prefix for this path.
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
protected setPrefix(): void {
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( this.initial.toLowerCase().startsWith('http://') ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
this.resourcePrefix = 'http://'
|
2021-06-02 01:59:40 +00:00
|
|
|
} else if ( this.initial.toLowerCase().startsWith('https://') ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
this.resourcePrefix = 'https://'
|
2021-06-02 01:59:40 +00:00
|
|
|
} else if ( this.filesystem ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
this.resourcePrefix = this.filesystem.getPrefix()
|
2021-06-02 01:59:40 +00:00
|
|
|
} else {
|
2021-06-03 03:36:25 +00:00
|
|
|
this.resourcePrefix = 'file://'
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine the "localized" string of this path.
|
|
|
|
*
|
|
|
|
* This is the normalized path WITHOUT the prefix.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* The normalized path of "file:///home/user/file.txt" is "/home/user/file.txt".
|
|
|
|
*
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
protected setLocal(): void {
|
|
|
|
this.resourceLocalPath = this.initial
|
|
|
|
if ( this.initial.toLowerCase().startsWith(this.resourcePrefix) ) {
|
|
|
|
this.resourceLocalPath = this.resourceLocalPath.slice(this.resourcePrefix.length)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( this.resourcePrefix === 'file://' && !this.resourceLocalPath.startsWith('/') && !this.filesystem ) {
|
|
|
|
this.resourceLocalPath = nodePath.resolve(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a new copy of this UniversalPath instance.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
clone(): UniversalPath {
|
2021-06-02 01:59:40 +00:00
|
|
|
return new UniversalPath(this.initial)
|
|
|
|
}
|
|
|
|
|
2021-10-18 17:48:16 +00:00
|
|
|
/**
|
|
|
|
* Get the URLSearchParams for this resource.
|
|
|
|
*/
|
|
|
|
get query(): URLSearchParams {
|
|
|
|
return this.resourceQuery
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
2021-06-03 03:36:25 +00:00
|
|
|
* Get the string of this resource.
|
2021-06-02 01:59:40 +00:00
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
get prefix(): string {
|
|
|
|
return this.resourcePrefix
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this resource refers to a file on the local filesystem.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
get isLocal(): boolean {
|
|
|
|
return this.resourcePrefix === 'file://' && !this.filesystem
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this resource refers to a file on a remote filesystem.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
get isRemote(): boolean {
|
|
|
|
return Boolean(this.resourcePrefix !== 'file://' || this.filesystem)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the non-prefixed path to this resource.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
get unqualified(): string {
|
|
|
|
return this.resourceLocalPath
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the path to this resource as it would be accessed from the current filesystem.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
get toLocal(): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( this.isLocal ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
return this.resourceLocalPath
|
2021-06-02 01:59:40 +00:00
|
|
|
} else {
|
2021-06-03 03:36:25 +00:00
|
|
|
return `${this.prefix}${this.resourceLocalPath}`
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the fully-prefixed path to this resource.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
get toRemote(): string {
|
2021-10-18 17:48:16 +00:00
|
|
|
const query = this.query.toString()
|
|
|
|
return `${this.prefix}${this.resourceLocalPath}${query ? '?' + query : ''}`
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* Get the basename of the path.
|
|
|
|
*/
|
|
|
|
get toBase(): string {
|
|
|
|
return nodePath.basename(this.resourceLocalPath)
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Append and resolve the given paths to this resource and return a new UniversalPath.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const homeDir = universalPath('home', 'user')
|
|
|
|
*
|
|
|
|
* homeDir.concat('file.txt').toLocal // => /home/user/file.txt
|
|
|
|
*
|
|
|
|
* homeDir.concat('..', 'other_user').toLocal // => /home/other_user
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param paths
|
|
|
|
*/
|
|
|
|
public concat(...paths: PathLike[]): UniversalPath {
|
|
|
|
const resolved = nodePath.join(this.unqualified, ...(paths.map(p => typeof p === 'string' ? p : p.unqualified)))
|
|
|
|
return new UniversalPath(`${this.prefix}${resolved}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Append the given path-like item to this resource's path.
|
|
|
|
* Unlike `concat`, this mutates the current instance.
|
|
|
|
* @param path
|
|
|
|
*/
|
|
|
|
public append(path: PathLike): this {
|
2021-06-03 03:36:25 +00:00
|
|
|
this.resourceLocalPath += String(path)
|
2021-06-02 01:59:40 +00:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cast the path to a string (fully-prefixed).
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
toString(): string {
|
|
|
|
return `${this.prefix}${this.resourceLocalPath}`
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the extension of the resource referred to by this instance.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const myFile = universalPath('home', 'user', 'file.txt')
|
|
|
|
*
|
|
|
|
* myFile.ext // => 'txt'
|
|
|
|
* ```
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
get ext(): string {
|
|
|
|
return nodePath.extname(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recursively walk all files in this directory. Must be a local resource.
|
|
|
|
*
|
|
|
|
* This returns an async generator function.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const configFiles = universalPath('home', 'user', '.config')
|
|
|
|
*
|
|
|
|
* for await (const configFile of configFiles.walk()) {
|
|
|
|
* // configFile is a string
|
|
|
|
* // ... do something ...
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
walk(): any {
|
|
|
|
return walk(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* Resolves true if this resource is a directory.
|
|
|
|
*/
|
|
|
|
async isDirectory(): Promise<boolean> {
|
|
|
|
if ( this.filesystem ) {
|
|
|
|
const stat = await this.filesystem.stat({
|
|
|
|
storePath: this.resourceLocalPath,
|
|
|
|
})
|
|
|
|
|
|
|
|
return stat.isDirectory
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
return (await fs.promises.stat(this.resourceLocalPath)).isDirectory()
|
|
|
|
} catch (e) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolves true if this resource is a regular file.
|
|
|
|
*/
|
|
|
|
async isFile(): Promise<boolean> {
|
|
|
|
if ( this.filesystem ) {
|
|
|
|
const stat = await this.filesystem.stat({
|
|
|
|
storePath: this.resourceLocalPath,
|
|
|
|
})
|
|
|
|
|
|
|
|
return stat.isFile
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
return (await fs.promises.stat(this.resourceLocalPath)).isFile()
|
|
|
|
} catch (e) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Returns true if the given resource exists at the path.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
async exists(): Promise<boolean> {
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( this.filesystem ) {
|
|
|
|
const stat = await this.filesystem.stat({
|
2021-06-03 03:36:25 +00:00
|
|
|
storePath: this.resourceLocalPath,
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return stat.exists
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2021-06-03 03:36:25 +00:00
|
|
|
await fs.promises.stat(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
return true
|
2021-06-03 03:36:25 +00:00
|
|
|
} catch (e) {
|
2021-06-02 01:59:40 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* List any immediate children of this resource.
|
|
|
|
*/
|
|
|
|
async list(): Promise<Collection<UniversalPath>> {
|
|
|
|
if ( this.filesystem ) {
|
|
|
|
const files = await this.filesystem.list(this.resourceLocalPath)
|
|
|
|
return files.map(x => this.concat(x))
|
|
|
|
}
|
|
|
|
|
|
|
|
const paths = await fs.promises.readdir(this.resourceLocalPath)
|
|
|
|
return Collection.collect<string>(paths)
|
|
|
|
.map(x => this.concat(x))
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Recursively create this path as a directory. Equivalent to `mkdir -p` on Linux.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
async mkdir(): Promise<void> {
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( this.filesystem ) {
|
|
|
|
await this.filesystem.mkdir({
|
2021-06-03 03:36:25 +00:00
|
|
|
storePath: this.resourceLocalPath,
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
} else {
|
2021-06-03 03:36:25 +00:00
|
|
|
await mkdirp(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Write the given data to this resource as a file.
|
|
|
|
* @param data
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
async write(data: string | Buffer): Promise<void> {
|
|
|
|
if ( typeof data === 'string' ) {
|
|
|
|
data = Buffer.from(data, 'utf8')
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
if ( this.filesystem ) {
|
|
|
|
const stream = await this.filesystem.putStoreFileAsStream({
|
2021-06-03 03:36:25 +00:00
|
|
|
storePath: this.resourceLocalPath,
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
await new Promise<void>((res, rej) => {
|
|
|
|
stream.write(data, err => {
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( err ) {
|
|
|
|
rej(err)
|
|
|
|
} else {
|
|
|
|
res()
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
} else {
|
2021-06-03 03:36:25 +00:00
|
|
|
const fd = await fs.promises.open(this.resourceLocalPath, 'w')
|
2021-06-02 01:59:40 +00:00
|
|
|
await fd.write(data)
|
|
|
|
await fd.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a writable stream to this file's contents.
|
|
|
|
*/
|
2021-07-08 01:13:23 +00:00
|
|
|
async writeStream(): Promise<Writable> {
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( this.filesystem ) {
|
|
|
|
return this.filesystem.putStoreFileAsStream({
|
2021-06-03 03:36:25 +00:00
|
|
|
storePath: this.resourceLocalPath,
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
} else {
|
2021-06-03 03:36:25 +00:00
|
|
|
return fs.createWriteStream(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read the data from this resource's file as a string.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
async read(): Promise<string> {
|
2021-07-08 01:13:23 +00:00
|
|
|
let stream: Readable
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( this.filesystem ) {
|
|
|
|
stream = await this.filesystem.getStoreFileAsStream({
|
2021-06-03 03:36:25 +00:00
|
|
|
storePath: this.resourceLocalPath,
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
} else {
|
2021-06-03 03:36:25 +00:00
|
|
|
stream = fs.createReadStream(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const chunks: any[] = []
|
|
|
|
return new Promise<string>((res, rej) => {
|
|
|
|
stream.on('data', chunk => chunks.push(Buffer.from(chunk)))
|
|
|
|
stream.on('error', rej)
|
|
|
|
stream.on('end', () => res(Buffer.concat(chunks).toString('utf-8')))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* Get the size of this resource in bytes.
|
|
|
|
*/
|
|
|
|
async sizeInBytes(): Promise<number> {
|
|
|
|
if ( this.filesystem ) {
|
|
|
|
const stat = await this.filesystem.stat({
|
|
|
|
storePath: this.resourceLocalPath,
|
|
|
|
})
|
|
|
|
|
|
|
|
if ( stat.exists ) {
|
|
|
|
return stat.sizeInBytes
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new FileNotFoundError(this.toString())
|
|
|
|
}
|
|
|
|
|
|
|
|
const stat = await fs.promises.stat(this.resourceLocalPath)
|
|
|
|
return stat.size
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the size of this resource, formatted in a human-readable string.
|
|
|
|
*/
|
|
|
|
async sizeForHumans(): Promise<string> {
|
|
|
|
return bytesToHumanFileSize(await this.sizeInBytes())
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Get a readable stream of this file's contents.
|
|
|
|
*/
|
2021-07-08 01:13:23 +00:00
|
|
|
async readStream(): Promise<Readable> {
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( this.filesystem ) {
|
|
|
|
return this.filesystem.getStoreFileAsStream({
|
2021-06-03 03:36:25 +00:00
|
|
|
storePath: this.resourceLocalPath,
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
} else {
|
2021-06-03 03:36:25 +00:00
|
|
|
return fs.createReadStream(this.resourceLocalPath)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* Returns true if this path exists in the subtree of the given path.
|
|
|
|
* @param otherPath
|
|
|
|
*/
|
|
|
|
isChildOf(otherPath: UniversalPath): boolean {
|
|
|
|
if ( (this.filesystem || otherPath.filesystem) && otherPath.filesystem !== this.filesystem ) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( this.prefix !== otherPath.prefix ) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const relative = nodePath.relative(otherPath.toLocal, this.toLocal)
|
|
|
|
return Boolean(relative && !relative.startsWith('..') && !nodePath.isAbsolute(relative))
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* Returns true if the given path exists in the subtree of this path.
|
|
|
|
* @param otherPath
|
|
|
|
*/
|
|
|
|
isParentOf(otherPath: UniversalPath): boolean {
|
|
|
|
return otherPath.isChildOf(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the given path refers to the same resource as this path.
|
|
|
|
* @param otherPath
|
|
|
|
*/
|
|
|
|
is(otherPath: UniversalPath): boolean {
|
|
|
|
if ( (this.filesystem || otherPath.filesystem) && otherPath.filesystem !== this.filesystem ) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( this.prefix !== otherPath.prefix ) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const relative = nodePath.relative(otherPath.toLocal, this.toLocal)
|
|
|
|
return relative === ''
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-07-08 01:13:23 +00:00
|
|
|
/**
|
|
|
|
* Get the mime-type of this resource.
|
|
|
|
*/
|
|
|
|
get mimeType(): string | false {
|
2021-07-08 03:50:48 +00:00
|
|
|
return mime.lookup(this.toBase)
|
2021-07-08 01:13:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the content-type header of this resource.
|
|
|
|
*/
|
|
|
|
get contentType(): string | false {
|
2021-07-08 03:50:48 +00:00
|
|
|
return mime.contentType(this.toBase)
|
2021-07-08 01:13:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the charset of this resource.
|
|
|
|
*/
|
|
|
|
get charset(): string | false {
|
|
|
|
if ( this.mimeType ) {
|
|
|
|
return mime.charset(this.mimeType)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
2021-07-08 01:13:23 +00:00
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
2021-10-18 17:48:16 +00:00
|
|
|
|
2022-01-17 21:57:40 +00:00
|
|
|
/**
|
|
|
|
* Return a new Pipe of this collection.
|
|
|
|
*/
|
|
|
|
pipeTo<TOut>(pipeline: Pipeline<this, TOut>): TOut {
|
|
|
|
return pipeline.apply(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Build and apply a pipeline. */
|
|
|
|
pipe<TOut>(builder: (pipeline: Pipeline<this, this>) => Pipeline<this, TOut>): TOut {
|
|
|
|
return builder(Pipeline.id()).apply(this)
|
2021-10-18 17:48:16 +00:00
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|