Create HTTPFilesystem implementation and add support to universalPath helper to automatically use it
Some checks failed
continuous-integration/drone Build is failing

This commit is contained in:
Garrett Mills 2023-06-13 01:09:33 -05:00
parent 7c9b1ff212
commit 899c8448fc
7 changed files with 157 additions and 16 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@extollo/lib", "name": "@extollo/lib",
"version": "0.14.11", "version": "0.14.12",
"description": "The framework library that lifts up your code.", "description": "The framework library that lifts up your code.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@ -33,6 +33,8 @@ export * from './support/debug'
export * from './support/path/Filesystem' export * from './support/path/Filesystem'
export * from './support/path/LocalFilesystem' export * from './support/path/LocalFilesystem'
export * from './support/path/SSHFilesystem' export * from './support/path/SSHFilesystem'
export * from './support/path/ReadOnlyFilesystem'
export * from './support/path/HTTPFilesystem'
export * from './support/Safe' export * from './support/Safe'
@ -43,3 +45,5 @@ export * from './support/global'
export * from './support/Pipe' export * from './support/Pipe'
export * from './support/Messages' export * from './support/Messages'
export * from './support/types' export * from './support/types'
export * from './support/path-helpers'

View File

@ -1,7 +1,7 @@
import {Logger} from './Logger' import {Logger} from './Logger'
import {LogMessage} from './types' import {LogMessage} from './types'
import {Injectable} from '../../di' import {Injectable} from '../../di'
import {universalPath} from '../support/path' import {universalPath} from '../support/path-helpers'
import {appPath, env} from '../../lifecycle/Application' import {appPath, env} from '../../lifecycle/Application'
import {Writable} from 'stream' import {Writable} from 'stream'

View File

@ -0,0 +1,21 @@
import {PathLike, UniversalPath} from './path'
import {HTTPFilesystem} from './path/HTTPFilesystem'
import {make} from '../../make'
import {Filesystem} from './path/Filesystem'
import {Maybe} from './types'
/**
* Create a new UniversalPath from the given path-like segments.
* @param parts
*/
export function universalPath(...parts: PathLike[]): UniversalPath {
let [main, ...concats] = parts // eslint-disable-line prefer-const
if ( !(main instanceof UniversalPath) ) {
let fs: Maybe<Filesystem> = undefined
if ( main.toLowerCase().startsWith('https://') || main.toLowerCase().startsWith('http://') ) {
fs = make(HTTPFilesystem)
}
main = new UniversalPath(main, fs)
}
return main.concat(...concats)
}

View File

@ -12,18 +12,6 @@ import {Pipeline} from './Pipe'
*/ */
export type PathLike = string | UniversalPath export type PathLike = string | UniversalPath
/**
* Create a new UniversalPath from the given path-like segments.
* @param parts
*/
export function universalPath(...parts: PathLike[]): UniversalPath {
let [main, ...concats] = parts // eslint-disable-line prefer-const
if ( !(main instanceof UniversalPath) ) {
main = new UniversalPath(main)
}
return main.concat(...concats)
}
/** /**
* Format bytes as human-readable text. * Format bytes as human-readable text.
* *
@ -144,7 +132,7 @@ export class UniversalPath {
* Return a new copy of this UniversalPath instance. * Return a new copy of this UniversalPath instance.
*/ */
clone(): UniversalPath { clone(): UniversalPath {
return new UniversalPath(this.initial) return new UniversalPath(this.initial, this.filesystem)
} }
/** /**
@ -224,7 +212,7 @@ export class UniversalPath {
*/ */
public concat(...paths: PathLike[]): UniversalPath { public concat(...paths: PathLike[]): UniversalPath {
const resolved = nodePath.join(this.unqualified, ...(paths.map(p => typeof p === 'string' ? p : p.unqualified))) const resolved = nodePath.join(this.unqualified, ...(paths.map(p => typeof p === 'string' ? p : p.unqualified)))
return new UniversalPath(`${this.prefix}${resolved}`) return new UniversalPath(`${this.prefix}${resolved}`, this.filesystem)
} }
/** /**

View File

@ -0,0 +1,86 @@
import * as fs from 'fs'
import * as mime from 'mime-types'
import * as path from 'path'
import {FilesystemOperationNotSupported, ReadOnlyFilesystem} from './ReadOnlyFilesystem'
import {UniversalPath} from '../path'
import {Awaitable, Maybe} from '../types'
import {Readable} from 'stream'
import {FileMetadata, Stat} from './Filesystem'
import {Collection} from '../../collection/Collection'
import {RequestInfo, RequestInit, Response} from 'node-fetch'
import {unsafeESMImport} from '../../unsafe'
const fetch = (url: RequestInfo, init?: RequestInit): Promise<Response> => unsafeESMImport('node-fetch').then(({default: nodeFetch}) => nodeFetch(url, init))
/**
* A filesystem implementation that reads files over HTTPS.
*/
export class HTTPFilesystem extends ReadOnlyFilesystem {
getPrefix(): string {
return 'https://'
}
async getStoreFileAsTemp(args: { storePath: string }): Promise<UniversalPath> {
const temp = this.tempName()
const write = fs.createWriteStream(temp)
const read = await this.getStoreFileAsStream(args)
return new Promise<UniversalPath>((res, rej) => {
write.on('finish', () => {
res(new UniversalPath(temp))
})
write.on('error', rej)
read.on('error', rej)
read.pipe(write)
})
}
async getStoreFileAsStream(args: { storePath: string }): Promise<Readable> {
if ( args.storePath.startsWith('/') ) {
args.storePath = args.storePath.slice(1)
}
const result = await fetch(`${this.getPrefix()}${args.storePath}`)
if ( result.body ) {
return (new Readable()).wrap(result.body)
}
return Readable.from([])
}
async stat(args: { storePath: string }): Promise<Stat> {
if ( args.storePath.startsWith('/') ) {
args.storePath = args.storePath.slice(1)
}
let response: Maybe<Response> = undefined
try {
response = await fetch(`${this.getPrefix()}${args.storePath}`)
} catch (e) {} // eslint-disable-line no-empty
return {
path: new UniversalPath(args.storePath, this),
exists: Boolean(response?.ok),
sizeInBytes: response?.size || 0,
mimeType: mime.lookup(path.basename(args.storePath)) || undefined,
tags: [],
accessed: undefined,
modified: undefined,
created: undefined,
isFile: true,
isDirectory: false,
}
}
getMetadata(storePath: string): Awaitable<FileMetadata> {
const mimeType = mime.lookup(path.basename(storePath))
return {
...(mimeType ? { mimeType } : {}),
}
}
list(): Awaitable<Collection<string>> {
throw new FilesystemOperationNotSupported(this, 'list')
}
}

View File

@ -0,0 +1,42 @@
import {ErrorWithContext} from '../../error/ErrorWithContext'
import {Filesystem} from './Filesystem'
import {Awaitable} from '../types'
import {Writable} from 'stream'
/**
* Error thrown when attempting to perform an operation on a filesystem that doesn't support it.
*/
export class FilesystemOperationNotSupported extends ErrorWithContext {
constructor(fs: Filesystem, operation: string, context: any = {}) {
super(`The filesystem ${fs.constructor.name} does not support the ${operation} operation.`, context)
}
}
/**
* Abstract base class for filesystems that are read-only (e.g. HTTP/S).
*/
export abstract class ReadOnlyFilesystem extends Filesystem {
putLocalFile(): Awaitable<void> {
throw new FilesystemOperationNotSupported(this, 'putLocalFile')
}
putStoreFileAsStream(): Awaitable<Writable> {
throw new FilesystemOperationNotSupported(this, 'putStoreFileAsStream')
}
touch(): Awaitable<void> {
throw new FilesystemOperationNotSupported(this, 'touch')
}
remove(): Awaitable<void> {
throw new FilesystemOperationNotSupported(this, 'remove')
}
mkdir(): Awaitable<void> {
throw new FilesystemOperationNotSupported(this, 'mkdir')
}
setMetadata(): Awaitable<void> {
throw new FilesystemOperationNotSupported(this, 'setMetadata')
}
}