File-based response support & static server
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Clean up UniversalPath implementation
- Use Readable/Writable types correctly for stream methods
- Add .list() methods for getting child files
- Make Response body specify explicit types and support
writing Readable streams to the body
- Create a static file server that supports directory listing
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import * as nodePath from 'path'
|
||||
import * as fs from 'fs'
|
||||
import * as mkdirp from 'mkdirp'
|
||||
import { Filesystem } from './path/Filesystem'
|
||||
import ReadableStream = NodeJS.ReadableStream;
|
||||
import WritableStream = NodeJS.WritableStream;
|
||||
import * as mime from 'mime-types'
|
||||
import {FileNotFoundError, Filesystem} from './path/Filesystem'
|
||||
import {Collection} from '../collection/Collection'
|
||||
import {Readable, Writable} from 'stream'
|
||||
|
||||
/**
|
||||
* An item that could represent a path.
|
||||
@@ -22,6 +23,36 @@ export function universalPath(...parts: PathLike[]): UniversalPath {
|
||||
return main.concat(...concats)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk recursively over entries in a directory.
|
||||
*
|
||||
@@ -155,6 +186,13 @@ export class UniversalPath {
|
||||
return `${this.prefix}${this.resourceLocalPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the basename of the path.
|
||||
*/
|
||||
get toBase(): string {
|
||||
return nodePath.basename(this.resourceLocalPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append and resolve the given paths to this resource and return a new UniversalPath.
|
||||
*
|
||||
@@ -224,6 +262,44 @@ export class UniversalPath {
|
||||
return walk(this.resourceLocalPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given resource exists at the path.
|
||||
*/
|
||||
@@ -244,6 +320,20 @@ export class UniversalPath {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively create this path as a directory. Equivalent to `mkdir -p` on Linux.
|
||||
*/
|
||||
@@ -290,7 +380,7 @@ export class UniversalPath {
|
||||
/**
|
||||
* Get a writable stream to this file's contents.
|
||||
*/
|
||||
async writeStream(): Promise<WritableStream> {
|
||||
async writeStream(): Promise<Writable> {
|
||||
if ( this.filesystem ) {
|
||||
return this.filesystem.putStoreFileAsStream({
|
||||
storePath: this.resourceLocalPath,
|
||||
@@ -304,7 +394,7 @@ export class UniversalPath {
|
||||
* Read the data from this resource's file as a string.
|
||||
*/
|
||||
async read(): Promise<string> {
|
||||
let stream: ReadableStream
|
||||
let stream: Readable
|
||||
if ( this.filesystem ) {
|
||||
stream = await this.filesystem.getStoreFileAsStream({
|
||||
storePath: this.resourceLocalPath,
|
||||
@@ -321,10 +411,37 @@ export class UniversalPath {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable stream of this file's contents.
|
||||
*/
|
||||
async readStream(): Promise<ReadableStream> {
|
||||
async readStream(): Promise<Readable> {
|
||||
if ( this.filesystem ) {
|
||||
return this.filesystem.getStoreFileAsStream({
|
||||
storePath: this.resourceLocalPath,
|
||||
@@ -334,17 +451,70 @@ export class UniversalPath {
|
||||
}
|
||||
}
|
||||
|
||||
/* get mime_type() {
|
||||
return Mime.lookup(this.ext)
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return Mime.contentType(this.ext)
|
||||
}
|
||||
|
||||
get charset() {
|
||||
if ( this.mime_type ) {
|
||||
return Mime.charset(this.mime_type)
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 === ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mime-type of this resource.
|
||||
*/
|
||||
get mimeType(): string | false {
|
||||
return mime.lookup(this.resourceLocalPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content-type header of this resource.
|
||||
*/
|
||||
get contentType(): string | false {
|
||||
return mime.contentType(this.resourceLocalPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the charset of this resource.
|
||||
*/
|
||||
get charset(): string | false {
|
||||
if ( this.mimeType ) {
|
||||
return mime.charset(this.mimeType)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user