UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

163 lines (147 loc) 4.85 kB
/* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ /* Render a public path */ // Do not backend render any file beyond this size, instead showing // a download link. This is to avoid a share server blocking for // a long time or using a lot of RAM. const MB: number = 1000000; const MAX_SIZE_MB: number = 10; // I want to raise this, but right now our Markdown and HTML renderers // are VERY slow on big files: const MAX_SIZE_HIGHLIGHT_MB: number = 0.5; import * as os_path from "path"; import { stat, readFile } from "fs"; import { callback } from "awaiting"; import { field_cmp, filename_extension, path_to_title } from "smc-util/misc"; import { React } from "smc-webapp/app-framework"; import { PublicPath } from "smc-webapp/share/public-path"; import { has_viewer, needs_content } from "smc-webapp/share/file-contents"; import { DirectoryListing } from "smc-webapp/share/directory-listing"; import { Author } from "smc-webapp/share/types"; import { get_listing } from "./listing"; import { redirect_to_directory } from "./util"; import { HostInfo } from "./public-paths"; import base_path from "smc-util-node/base-path"; export async function render_public_path(opts: { req: any; res: any; // html response object info?: HostInfo; // immutable.js info about the public share, if url starts with share id (as opposed to project_id) dir: string; // directory on disk containing files for this path react: Function; path: string; viewer: string; hidden?: boolean; sort: string; // e.g., '-mtime' = sort files in reverse by timestamp authors: Author[]; views?: number; }): Promise<void> { const path_to_file = os_path.join(opts.dir, opts.path); function dbg(...args): void { console.log(`render_public_path('${path_to_file}')`, ...args); } dbg(); let stats; try { stats = await callback(stat, path_to_file); } catch (err) { dbg("error", err); opts.res.sendStatus(404); return; } if (stats.isDirectory()) { dbg("is directory"); if (opts.path.slice(-1) !== "/") { redirect_to_directory(opts.req, opts.res); return; } let files; try { files = await get_listing(path_to_file); } catch (err) { // TODO: show directory listing opts.res.send(`Error getting directory listing -- ${err}`); return; } let reverse: boolean; let sort: string; if (opts.sort[0] === "-") { reverse = true; sort = opts.sort.slice(1); } else { reverse = false; sort = opts.sort; } files.sort(field_cmp(sort)); if (reverse) { files.reverse(); } const component = React.createElement(DirectoryListing, { hidden: opts.hidden, info: opts.info as any, // typescript gets confused between two copies of immutable, breaking checking in this case. files: files, viewer: opts.viewer, path: opts.path, views: opts.views, base_path, }); // NOTE: last true is because we never index directory listings -- instead we want // users to find specific files by their content and name opts.react(opts.res, component, opts.path, true); return; } dbg("is file"); let noindex: boolean; if (opts.viewer == "share") { // do index a file if we will be showing it via the share server. noindex = false; } else { noindex = true; // never index content that is raw or embedded } let why: string | undefined = undefined; let content: string | undefined = undefined; let ext = filename_extension(path_to_file); if (ext != null) ext = ext.toLowerCase(); if (!has_viewer(ext)) { why = "We do not have a way to display this file."; } else if (stats.size > MAX_SIZE_MB * MB) { why = "File too big to be shown."; } else { if (needs_content(ext)) { try { content = (await callback(readFile, path_to_file)).toString(); } catch (err) { opts.res.sendStatus(404); return; } } else { content = ""; } } let highlight: boolean; if (ext == "ipynb") { // ipynb files tend to be very large, but still easy to render, due to images. // This is a little dangerous though! We will eventually need to do something // maybe async with a timeout... highlight = true; } else { highlight = stats.size < MAX_SIZE_HIGHLIGHT_MB * MB; } const component = React.createElement(PublicPath, { info: opts.info as any, // see comment where this is done above. content, viewer: opts.viewer, path: opts.path, why, size: stats.size, highlight, authors: opts.authors, base_path, views: opts.views, }); const subtitle = path_to_title(opts.path); opts.react(opts.res, component, subtitle, noindex); }