UNPKG

@webda/core

Version:

Expose API with Lambda

371 lines 13.1 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import * as fs from "fs"; import { join } from "path"; import { CloudBinary, CloudBinaryParameters, WebdaError } from "../index.js"; import { BinaryNotFoundError, MemoryBinaryFile } from "./binary.js"; import { Inject } from "./service.js"; export class FileBinaryParameters extends CloudBinaryParameters { constructor(params, service) { super(params, service); if (!this.folder.endsWith("/")) { this.folder += "/"; } this.maxSize ?? (this.maxSize = 10 * 1024 * 1024); // Remove trailing / if (this.url && this.url.endsWith("/")) { this.url = this.url.substring(0, this.url.length - 1); } } } /** * FileBinary handles the storage of binary on a hard drive * * The structure used for now is * /folder/{hash}/data * /folder/{hash}/{targetStore}_{uuid} * /folder/{hash}/challenge * * It takes one parameter * folder: "path" * * See Binary the general interface * @category CoreServices * @WebdaModda */ export class FileBinary extends CloudBinary { /** * Load parameters * * @param params */ loadParameters(params) { return new FileBinaryParameters(params, this); } /** * Create the storage folder if needed */ computeParameters() { super.computeParameters(); if (!fs.existsSync(this.parameters.folder)) { fs.mkdirSync(this.parameters.folder, { recursive: true }); } } initRoutes() { super.initRoutes(); // We do not want to expose by default if (this.parameters.url === undefined) { return; } // Will redirect to this URL for direct download let url = this.parameters.url + "/download/data/{hash}{?token,content-disposition,content-type}"; let name = this.getOperationName(); this.addRoute(url, ["GET"], this.downloadBinaryLink, { put: { operationId: `get${name}Binary`, description: "Download a binary to an object after challenge", summary: "Download a binary", responses: { "200": { description: "Content of binary" }, "403": { description: "Wrong hash" } } } }); url = this.parameters.url + "/upload/data/{hash}{?token}"; this.addRoute(url, ["PUT"], this.storeBinary, { put: { operationId: `put${name}Binary`, description: "Upload a binary to an object after challenge", summary: "Upload a binary", responses: { "204": { description: "" }, "400": { description: "Wrong hash" }, "404": { description: "Object does not exist or attachment does not exist" }, "412": { description: "Provided hash does not match" } } } }); } /** * Download a binary with a challenge */ async downloadBinaryLink(ctx) { const { hash } = ctx.getPathParameters(); // Verify token try { let dt = await this.cryptoService.jwtVerify(ctx.parameter("token")); if (dt.hash !== hash) { throw new WebdaError.Forbidden("Wrong hash"); } } catch (err) { throw new WebdaError.Forbidden("Incorrect JWT"); } ctx.writeHead(200, { "content-disposition": ctx.parameter("content-disposition"), "content-type": ctx.parameter("content-type") }); let map = await this.get({ hash }); // Fake a binary map for this case return new Promise((resolve, reject) => { const stream = ctx.getStream(); stream.on("error", reject).on("finish", resolve); map.pipe(stream); }); } /** * @inheritdoc */ async _get(info) { let path = this._getPath(info.hash, "data"); if (!fs.existsSync(path)) { throw new BinaryNotFoundError(info.hash, this.getName()); } return fs.createReadStream(path); } /** * Get signed url to download an element */ async getSignedUrlFromMap(map, expires, context) { return `${context .getHttpContext() .getAbsoluteUrl(this.parameters.url + "/download/data/" + map.hash)}?token=${await this.getToken(map.hash, "GET", expires)}&content-type=${map.mimetype}&content-disposition=attachment`; } /** * Get storage path for a hash * @param hash * @param postfix * @returns */ _getPath(hash, postfix = undefined) { if (postfix === undefined) { return this.parameters.folder + hash; } return this.parameters.folder + hash + "/" + postfix; } _touch(filePath, mode = 0o600) { try { const fd = fs.openSync(filePath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR, mode); fs.closeSync(fd); } catch (e) { // file existed } } /** * Get token for action * * @param ctx * @param expiresIn */ async getToken(hash, method, expiresIn = 60) { return this.cryptoService.jwtSign({ hash, method }, { expiresIn }); } /** * Get put URL * @param ctx * @returns */ async getPutUrl(ctx) { let body = await ctx.getRequestBody(); // Get a full URL, this method should be in a Route Object // Add a JWT token for 60s let token = await this.getToken(body.hash, "PUT"); return ctx.getHttpContext().getAbsoluteUrl(this.parameters.url + "/upload/data/" + body.hash + `?token=${token}`); } /** * Will give you the redirect url * * @ignore */ async putRedirectUrl(ctx) { const body = await ctx.getInput(); const { uuid, store, property } = ctx.getParameters(); let result = { url: await this.getPutUrl(ctx), method: "PUT" }; if (fs.existsSync(this._getPath(body.hash, store + "_" + property + "_" + uuid))) { if (!fs.existsSync(this._getPath(body.hash, "data"))) { // 232-237 return result; } // If the link is already register just return directly ok return; } // Get the target object to add the mapping let targetStore = this._verifyMapAndStore(ctx); let object = await targetStore.get(uuid, ctx); await this.uploadSuccess(object, property, body); // Need to store the usage of the file if (!fs.existsSync(this._getPath(body.hash))) { fs.mkdirSync(this._getPath(body.hash)); } this._touch(this._getPath(body.hash, store + "_" + uuid + "_" + property)); if (this.challenge(body.hash, body.challenge)) { // Return empty as we dont need to upload the data return; } // Return the url to upload the binary now return result; } /** * Store the binary sent * * Check the hashs match and that a storage folder exists * The storage folder should have been created by the putRedirectUrl * * @ignore */ async storeBinary(ctx) { let body = await ctx.getHttpContext().getRawBody(this.parameters.maxSize); let result = await new MemoryBinaryFile(body, { mimetype: (ctx.getHttpContext().getUniqueHeader("content-type") || "application/json").split(";")[0], name: "", size: parseInt(ctx.getHttpContext().getUniqueHeader("content-length")) }).getHashes(); if (ctx.parameter("hash") !== result.hash) { this.log("WARN", "Request hash differ", ctx.parameter("hash"), "!==", result.hash); throw new WebdaError.BadRequest("Request hash differ"); } // Verify token try { let dt = await this.cryptoService.jwtVerify(ctx.parameter("token")); if (dt.hash !== result.hash) { this.log("WARN", "JWT hash differ", ctx.parameter("hash"), "!==", result.hash); throw new WebdaError.Forbidden("JWT hash differ"); } } catch (err) { this.log("WARN", "Invalid JWT token"); throw new WebdaError.Forbidden("Invalid JWT token"); } if (!fs.existsSync(this._getPath(result.hash))) { // The folder should have been create by a previous request throw new WebdaError.PreconditionFailed("No folder for hash " + result.hash); } let path = this._getPath(result.hash, "data"); if (!fs.existsSync(path)) { // Save the data fs.writeFileSync(path, body); } // Save the challenge this._touch(this._getPath(result.hash, "_" + result.challenge)); } /** * @inheritdoc */ async getUsageCount(hash) { let path = this._getPath(hash); if (!fs.existsSync(path)) { return 0; } let files = fs.readdirSync(path); return files.length - 2; } /** * Delete any usage marker and hash * * @param hash to remove * @returns */ async _cleanHash(hash) { const p = this._getPath(hash); if (!fs.existsSync(p)) return; try { let files = fs.readdirSync(p); files.forEach(file => fs.unlinkSync(join(p, file))); fs.rmdirSync(p); } catch (err) { } } /** * @override */ async _cleanUsage(hash, uuid, attribute) { const p = this._getPath(hash); if (!fs.existsSync(p)) return; uuid = uuid.startsWith("_") ? uuid : `_${uuid}`; let files = fs.readdirSync(p); files .filter(f => f.endsWith(attribute ? `_${attribute}${uuid}` : `${uuid}`)) .forEach(f => fs.unlinkSync(this._getPath(hash, f))); if (files.length == 3) { await this._cleanHash(hash); } } /** * @inheritdoc */ async delete(object, property, index) { let hash = (index !== undefined ? object[property][index] : object[property]).hash; await this.deleteSuccess(object, property, index); await this._cleanUsage(hash, object.getUuid(), property); } challenge(hash, challenge) { let path = this._getPath(hash); return fs.existsSync(path) && fs.existsSync(`${path}/_${challenge}`); } /** * @inheritdoc */ async cascadeDelete(info, uuid) { return this._cleanUsage(info.hash, uuid); } async _store(file, object, attribute) { fs.mkdirSync(this._getPath(file.hash)); const map = await file.get(); await new Promise((resolve, reject) => { map .pipe(fs.createWriteStream(this._getPath(file.hash, "data"))) .on("error", reject) .on("finish", resolve); }); // Store the challenge this._touch(this._getPath(file.hash, "_" + file.challenge)); this._touch(this._getPath(file.hash, `${object.getStore().getName()}_${attribute}_${object.getUuid()}`)); } /** * @inheritdoc */ async store(object, property, file) { await file.getHashes(); const storeName = object.getStore().getName(); const fileInfo = file.toBinaryFileInfo(); this.checkMap(object, property); if (fs.existsSync(this._getPath(file.hash))) { this._touch(this._getPath(file.hash, `${storeName}_${property}_${object.getUuid()}`)); await this.uploadSuccess(object, property, fileInfo); return; } await this._store(file, object, property); await this.uploadSuccess(object, property, fileInfo); } /** * Clean all data */ async ___cleanData() { (await import("fs-extra")).emptyDirSync(this.parameters.folder); } } __decorate([ Inject("CryptoService") ], FileBinary.prototype, "cryptoService", void 0); //# sourceMappingURL=filebinary.js.map