@adonisjs/drive
Version:
A thin wrapper on top of Flydrive to work seamlessly with AdonisJS
200 lines (196 loc) • 6.87 kB
JavaScript
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
};