Path: blob/master/src/packages/hub/proxy/file-download.ts
1710 views
import { readFile as readProjectFile } from "@cocalc/conat/files/read";1import { once } from "events";2import { path_split } from "@cocalc/util/misc";3import mime from "mime-types";4import getLogger from "../logger";56const DANGEROUS_CONTENT_TYPE = new Set(["image/svg+xml" /*, "text/html"*/]);78const logger = getLogger("hub:proxy:file-download");910// assumes request has already been authenticated!1112export async function handleFileDownload(req, res, url, project_id) {13logger.debug("handling the request via conat file streaming", url);14const i = url.indexOf("files/");15const compute_server_id = req.query.id ?? 0;16let j = url.lastIndexOf("?");17if (j == -1) {18j = url.length;19}20const path = decodeURIComponent(url.slice(i + "files/".length, j));21logger.debug("conat: get file", { project_id, path, compute_server_id, url });22const fileName = path_split(path).tail;23const contentType = mime.lookup(fileName);24if (req.query.download != null || DANGEROUS_CONTENT_TYPE.has(contentType)) {25const fileNameEncoded = encodeURIComponent(fileName)26.replace(/['()]/g, escape)27.replace(/\*/g, "%2A");28res.setHeader(29"Content-disposition",30`attachment; filename*=UTF-8''${fileNameEncoded}`,31);32}33res.setHeader("Content-type", contentType);3435let headersSent = false;36res.on("finish", () => {37headersSent = true;38});39try {40for await (const chunk of await readProjectFile({41project_id,42compute_server_id,43path,44// allow a long download time (1 hour), since files can be large and45// networks can be slow.46maxWait: 1000 * 60 * 60,47})) {48if (res.writableEnded || res.destroyed) {49break;50}51if (!res.write(chunk)) {52await once(res, "drain");53}54}55res.end();56} catch (err) {57logger.debug(58"ERROR streaming file",59{ project_id, compute_server_id, path },60err,61);62if (!headersSent) {63res.statusCode = 500;64res.end("Error reading file.");65} else {66// Data sent, forcibly kill the connection67res.destroy(err);68}69}70}717273