UNPKG

@flystorage/file-storage

Version:

File-storage abstraction: multiple filesystems, one API.

426 lines (425 loc) 18.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PreparedUploadsAreNotSupported = exports.FileStorage = exports.DirectoryListing = void 0; exports.isFile = isFile; exports.isDirectory = isDirectory; exports.toReadable = toReadable; exports.normalizeExpiryToDate = normalizeExpiryToDate; exports.normalizeExpiryToMilliseconds = normalizeExpiryToMilliseconds; exports.closeReadable = closeReadable; exports.readableToString = readableToString; exports.readableToBuffer = readableToBuffer; exports.readableToUint8Array = readableToUint8Array; const stream_1 = require("stream"); const checksum_from_stream_js_1 = require("./checksum-from-stream.js"); const path_normalizer_js_1 = require("./path-normalizer.js"); const util_1 = require("util"); const errors_js_1 = require("./errors.js"); const node_stream_1 = require("node:stream"); function isFile(stat) { return stat.isFile; } function isDirectory(stat) { return stat.isDirectory; } class DirectoryListing { listing; path; deep; constructor(listing, path, deep) { this.listing = listing; this.path = path; this.deep = deep; } async toArray(sorted = true) { const items = []; for await (const item of this.listing) { items.push(item); } return sorted ? items.sort((a, b) => naturalSorting.compare(a.path, b.path)) : items; } filter(filter) { const listing = this.listing; const filtered = (async function* () { for await (const entry of listing) { if (filter(entry)) { yield entry; } } })(); return new DirectoryListing(filtered, this.path, this.deep); } async *[Symbol.asyncIterator]() { try { for await (const item of this.listing) { yield item; } } catch (error) { throw errors_js_1.UnableToListDirectory.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path: this.path, deep: this.deep } }); } } } exports.DirectoryListing = DirectoryListing; function toReadable(contents) { if (contents instanceof stream_1.Readable) { return contents; } return stream_1.Readable.from(contents); } const naturalSorting = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base', }); function instrumentAbortSignal(options) { let abortSignal = options.abortSignal; if (options.timeout !== undefined) { const timeoutAbort = AbortSignal.timeout(options.timeout); if (options.abortSignal) { const originalAbortSignal = options.abortSignal; abortSignal = AbortSignal.any([ originalAbortSignal, timeoutAbort, ]); } else { abortSignal = timeoutAbort; } } if (abortSignal?.aborted) { throw abortSignal.reason; } return { ...options, abortSignal }; } class FileStorage { adapter; pathNormalizer; options; constructor(adapter, pathNormalizer = new path_normalizer_js_1.PathNormalizerV1(), options = {}) { this.adapter = adapter; this.pathNormalizer = pathNormalizer; this.options = options; } async write(path, contents, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.visibility, ...this.options.writes, ...options }); try { const body = toReadable(contents); await this.adapter.write(this.pathNormalizer.normalizePath(path), body, options); await closeReadable(body); } catch (error) { throw errors_js_1.UnableToWriteFile.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } async read(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { const stream = stream_1.Readable.from(await this.adapter.read(this.pathNormalizer.normalizePath(path), options)); const streamOut = new node_stream_1.PassThrough(); stream.on('error', (error) => { stream.unpipe(streamOut); streamOut.destroy((0, errors_js_1.isFileWasNotFound)(error) ? errors_js_1.UnableToReadFile.becauseFileWasNotFound(error) : error); }); stream.pipe(streamOut); return streamOut; } catch (error) { if ((0, errors_js_1.isFileWasNotFound)(error)) { throw errors_js_1.UnableToReadFile.becauseFileWasNotFound(error); } throw errors_js_1.UnableToReadFile.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async readToString(path, options = {}) { return await readableToString(await this.read(path, options)); } async readToUint8Array(path, options = {}) { return await readableToUint8Array(await this.read(path, options)); } async readToBuffer(path, options = {}) { return Buffer.from(await this.readToUint8Array(path, options)); } async deleteFile(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { await this.adapter.deleteFile(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToDeleteFile.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async createDirectory(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.visibility, ...options }); try { return await this.adapter.createDirectory(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToCreateDirectory.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } async deleteDirectory(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { return await this.adapter.deleteDirectory(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToDeleteDirectory.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async stat(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { return await this.adapter.stat(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToGetStat.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async moveFile(from, to, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.visibility, ...this.options.moves, ...options }); try { await this.adapter.moveFile(this.pathNormalizer.normalizePath(from), this.pathNormalizer.normalizePath(to), options); } catch (error) { throw errors_js_1.UnableToMoveFile.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { from, to } }); } } async copyFile(from, to, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.visibility, ...this.options.copies, ...options }); try { await this.adapter.copyFile(this.pathNormalizer.normalizePath(from), this.pathNormalizer.normalizePath(to), options); } catch (error) { throw errors_js_1.UnableToCopyFile.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { from, to } }); } } async changeVisibility(path, visibility, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); if (options.useVisibility === false) { const fallback = options.visibilityFallback; if (fallback.strategy === 'ignore') { return; } else if (fallback.strategy === 'error') { throw errors_js_1.UnableToSetVisibility.because(fallback.errorMessage ?? 'Configured not to use visibility', { context: { path, visibility }, }); } } try { return await this.adapter.changeVisibility(this.pathNormalizer.normalizePath(path), visibility, options); } catch (error) { throw errors_js_1.UnableToSetVisibility.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, visibility } }); } } async visibility(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.visibility, ...options }); if (options.useVisibility === false) { const fallback = options.visibilityFallback; if (fallback.strategy === 'ignore') { return fallback.stagedVisibilityResponse ?? 'unknown'; } else if (fallback.strategy === 'error') { throw errors_js_1.UnableToGetVisibility.because(fallback.errorMessage ?? 'Configured not to use visibility', { context: { path }, }); } } try { return await this.adapter.visibility(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToGetVisibility.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async fileExists(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { return await this.adapter.fileExists(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToCheckFileExistence.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } list(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.list, ...options }); const adapterOptions = { ...options, deep: options.deep ?? false, }; return new DirectoryListing(this.adapter.list(this.pathNormalizer.normalizePath(path), adapterOptions), path, adapterOptions.deep); } async statFile(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); const stat = await this.stat(path, options); if (isFile(stat)) { return stat; } throw errors_js_1.UnableToGetStat.noFileStatResolved({ context: { path } }); } async directoryExists(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { return await this.adapter.directoryExists(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToCheckDirectoryExistence.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async publicUrl(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.publicUrls, ...options }); try { return await this.adapter.publicUrl(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToGetPublicUrl.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } async temporaryUrl(path, options) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.temporaryUrls, ...options }); try { return await this.adapter.temporaryUrl(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToGetTemporaryUrl.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } async prepareUpload(path, options) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.uploadRequest, ...options }); if (this.options.preparedUploadStrategy !== undefined) { try { return this.options.preparedUploadStrategy.prepareUpload(path, options); } catch (error) { throw errors_js_1.UnableToPrepareUploadRequest.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } if (typeof this.adapter.prepareUpload !== 'function') { throw new Error('The used adapter does not support prepared uploads.'); } try { return await this.adapter.prepareUpload(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToPrepareUploadRequest.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } async checksum(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.checksums, ...options }); try { return await this.adapter.checksum(this.pathNormalizer.normalizePath(path), options); } catch (error) { if (errors_js_1.ChecksumIsNotAvailable.isErrorOfType(error)) { return this.calculateChecksum(path, options); } throw errors_js_1.UnableToGetChecksum.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } async mimeType(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...this.options.mimeTypes, ...options }); try { return await this.adapter.mimeType(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToGetMimeType.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } async lastModified(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { return await this.adapter.lastModified(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToGetLastModified.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async fileSize(path, options = {}) { options = instrumentAbortSignal({ ...this.options.timeout, ...options }); try { return await this.adapter.fileSize(this.pathNormalizer.normalizePath(path), options); } catch (error) { throw errors_js_1.UnableToGetFileSize.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path } }); } } async calculateChecksum(path, options) { try { return await (0, checksum_from_stream_js_1.checksumFromStream)(await this.read(path, options), options); } catch (error) { throw errors_js_1.UnableToGetChecksum.because((0, errors_js_1.errorToMessage)(error), { cause: error, context: { path, options } }); } } } exports.FileStorage = FileStorage; function normalizeExpiryToDate(expiresAt) { return expiresAt instanceof Date ? expiresAt : new Date(expiresAt); } function normalizeExpiryToMilliseconds(expiresAt) { return expiresAt instanceof Date ? expiresAt.getTime() : expiresAt; } async function closeReadable(body) { if (body.closed || body.destroyed) { return; } await new Promise((resolve, reject) => { body.on('error', reject); body.on('close', (err) => { err ? reject(err) : resolve(); }); body.destroy(); }); } const decoder = new TextDecoder(); async function readableToString(stream) { const contents = decoder.decode(await readableToUint8Array(stream)); await closeReadable(stream); return contents; } async function readableToBuffer(stream) { return new Promise((resolve, reject) => { const buffers = []; stream.on('data', chunk => buffers.push(Buffer.from(chunk))); stream.on('end', () => resolve(Buffer.concat(buffers))); stream.on('finish', () => resolve(Buffer.concat(buffers))); stream.on('error', err => reject(err)); }); } const encoder = new util_1.TextEncoder(); function readableToUint8Array(stream) { return new Promise((resolve, reject) => { const parts = []; stream.on('data', (chunk) => { const type = typeof chunk; if (type === 'string') { chunk = encoder.encode(chunk); } else if (type === 'number') { chunk = new Uint8Array([chunk]); } parts.push(chunk); }); stream.on('error', reject); stream.on('end', () => resolve(concatUint8Arrays(parts))); }); } function concatUint8Arrays(input) { const length = input.reduce((l, a) => l + (a.byteLength), 0); const output = new Uint8Array(length); let position = 0; input.forEach(i => { output.set(i, position); position += i.byteLength; }); return output; } class PreparedUploadsAreNotSupported { prepareUpload() { throw new Error('The used adapter does not support prepared uploads.'); } } exports.PreparedUploadsAreNotSupported = PreparedUploadsAreNotSupported;