UNPKG

@adonisjs/drive

Version:

A thin wrapper on top of Flydrive to work seamlessly with AdonisJS

200 lines (196 loc) 6.87 kB
import { CannotServeFileException, debug_default } from "../chunk-GRHMQSPS.js"; import "../chunk-MLKGABMK.js"; // providers/drive_provider.ts import { readFile } from "node:fs/promises"; import { Disk, DriveManager } from "flydrive"; import { configProvider } from "@adonisjs/core"; import { MultipartFile } from "@adonisjs/core/bodyparser"; import { RuntimeException } from "@adonisjs/core/exceptions"; // src/file_server.ts function decodeLocation(location) { try { return decodeURIComponent(location); } catch { return location; } } function createFileServer(disk) { return async function({ request, response }) { const location = decodeLocation(request.param("*").join("/")); const file = disk.file(location); const visibility = await file.getVisibility(); const isPrivate = visibility === "private"; const usingSignature = !!request.input("signature"); const hasValidSignature = request.hasValidSignature(); if (isPrivate && !hasValidSignature || usingSignature && !hasValidSignature) { debug_default('Access denied for file "%s". Failed condition %o', location, { isPrivate, hasValidSignature, usingSignature }); return response.unauthorized("Access denied"); } try { const metadata = await file.getMetaData(); response.header("etag", metadata.etag); if (usingSignature && request.input("cacheControl")) { response.header("Cache-Control", request.input("cacheControl")); } else { response.header("Last-Modified", metadata.lastModified.toUTCString()); } if (usingSignature && request.input("contentType")) { response.header("Content-Type", request.input("contentType")); } else if (metadata.contentType) { response.type(metadata.contentType); } if (usingSignature && request.input("contentDisposition")) { response.header("Content-Disposition", request.input("contentDisposition")); } if (usingSignature && request.input("contentEncoding")) { response.header("Content-Encoding", request.input("contentEncoding")); } if (usingSignature && request.input("contentLanguage")) { response.header("Content-Language", request.input("contentLanguage")); } if (request.method() === "HEAD") { response.status(response.fresh() ? 304 : 200); return; } if (response.fresh()) { response.status(304); return; } response.header("Content-length", metadata.contentLength.toString()); return response.stream(await file.getStream()); } catch (error) { throw new CannotServeFileException(error); } }; } // providers/drive_provider.ts var DriveProvider = class { constructor(app) { this.app = app; } /** * Collection of services using the "fs" driver and want * to serve files using the AdonisJS HTTP server. */ #locallyServedServices = []; /** * Defines the template engine on the message class to * render templates */ async registerViewHelpers(drive) { if (this.app.usingEdgeJS) { const edge = await import("edge.js"); debug_default("detected edge installation. Registering drive global helpers"); edge.default.global("driveUrl", function(key, diskName) { const disk = diskName ? drive.use(diskName) : drive.use(); return disk.getUrl(key); }); edge.default.global( "driveSignedUrl", function(key, diskNameOrOptions, signedUrlOptions) { let diskName; let options = signedUrlOptions; if (typeof diskNameOrOptions === "string") { diskName = diskNameOrOptions; } else if (diskNameOrOptions && !signedUrlOptions) { options = diskNameOrOptions; } const disk = diskName ? drive.use(diskName) : drive.use(); return disk.getSignedUrl(key, options); } ); } } /** * Extending BodyParser Multipart file with "moveToDisk" * method to move file from the local filesystem to * a drive disk */ async extendMultipartFile(drive) { debug_default('Adding "MultipartFile.moveToDisk" method'); MultipartFile.macro( "moveToDisk", async function(key, diskNameOrOptions, writeOptions) { if (!this.tmpPath) { throw new RuntimeException( 'property "tmpPath" must be set on the file before moving it', { status: 500, code: "E_MISSING_FILE_TMP_PATH" } ); } let diskName; let options = {}; if (typeof diskNameOrOptions === "string") { diskName = diskNameOrOptions; options = writeOptions ?? {}; } else if (diskNameOrOptions && !writeOptions) { options = diskNameOrOptions; } else if (writeOptions) { options = writeOptions; } const moveAs = options.moveAs ?? "stream"; const disk = diskName ? drive.use(diskName) : drive.use(); if (moveAs === "stream") { await disk.moveFromFs(this.tmpPath, key, options); } else { await disk.put(key, await readFile(this.tmpPath), options); } try { this.meta.url = await disk.getUrl(key); } catch { } this.markAsMoved(key, key); } ); } register() { this.app.container.singleton("drive.manager", async () => { const driveConfigProvider = this.app.config.get("drive"); const config = await configProvider.resolve(this.app, driveConfigProvider); if (!config) { throw new RuntimeException( 'Invalid "config/drive.ts" file. Make sure you are using the "defineConfig" method' ); } this.#locallyServedServices = config.locallyServed; return new DriveManager(config.config); }); this.app.container.bind(Disk, async (resolver) => { const driveManager = await resolver.make("drive.manager"); return driveManager.use(); }); } /** * The boot method resolves drive and router to register * the routes for the locally served services. * * The routes must be defined before the application has * started. */ async boot() { const drive = await this.app.container.make("drive.manager"); const router = await this.app.container.make("router"); this.#locallyServedServices.forEach((service) => { debug_default( 'configuring drive local file server for "%s", route "%s"', service.service, service.routePattern ); router.get(service.routePattern, createFileServer(drive.use(service.service))).as(service.routeName); }); await this.registerViewHelpers(drive); await this.extendMultipartFile(drive); } }; export { DriveProvider as default };