CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/hub/servers/app/blob-upload.ts
Views: 923
1
/*
2
Support user uploading a blob directly from their browser to the CoCalc database,
3
mainly for markdown documents. This is meant to be very similar to how GitHub
4
allows for attaching files to github issue comments.
5
*/
6
7
// See also src/packages/project/upload.ts
8
9
import { Router } from "express";
10
import { callback2 } from "@cocalc/util/async-utils";
11
import { database } from "../database";
12
const { save_blob } = require("@cocalc/hub/blobs");
13
import { getLogger } from "@cocalc/hub/logger";
14
import {
15
MAX_BLOB_SIZE,
16
//MAX_BLOB_SIZE_PER_PROJECT_PER_DAY,
17
} from "@cocalc/util/db-schema/blobs";
18
import getAccount from "@cocalc/server/auth/get-account";
19
import isCollaborator from "@cocalc/server/projects/is-collaborator";
20
import formidable from "formidable";
21
import { readFile, unlink } from "fs/promises";
22
import { uuidsha1 } from "@cocalc/backend/misc_node";
23
24
const logger = getLogger("hub:servers:app:blob-upload");
25
function dbg(...args): void {
26
logger.debug("upload ", ...args);
27
}
28
29
export default function init(router: Router) {
30
router.post("/blobs", async (req, res) => {
31
const account_id = await getAccount(req);
32
if (!account_id) {
33
res.status(500).send("user must be signed in to upload files");
34
return;
35
}
36
const { project_id, ttl } = req.query;
37
if (typeof project_id == 'string' && project_id) {
38
if (!(await isCollaborator({ account_id, project_id }))) {
39
res.status(500).send("user must be collaborator on project");
40
return;
41
}
42
}
43
44
dbg({ account_id, project_id });
45
46
// TODO: check for throttling/limits
47
try {
48
const form = formidable({
49
keepExtensions: true,
50
maxFileSize: MAX_BLOB_SIZE,
51
hashAlgorithm: "sha1",
52
});
53
54
dbg("parsing form data...");
55
// https://github.com/node-formidable/formidable?tab=readme-ov-file#parserequest-callback
56
const [_, files] = await form.parse(req);
57
//dbg(`finished parsing form data. ${JSON.stringify({ fields, files })}`);
58
59
/* Just for the sake of understanding this, this is how this looks like in the real world (formidable@3):
60
> files.file[0]
61
{
62
size: 80789,
63
filepath: '/home/hsy/p/cocalc/src/data/projects/c8787b71-a85f-437b-9d1b-29833c3a199e/asdf/asdf/8e3e4367333e45275a8d1aa03.png',
64
newFilename: '8e3e4367333e45275a8d1aa03.png',
65
mimetype: 'application/octet-stream',
66
mtime: '2024-04-23T09:25:53.197Z',
67
originalFilename: 'Screenshot from 2024-04-23 09-20-40.png'
68
}
69
*/
70
let uuid: string | undefined = undefined;
71
if (files.file?.[0] != null) {
72
const { filepath, hash } = files.file[0];
73
try {
74
dbg("got", files);
75
if (typeof hash == "string") {
76
uuid = uuidsha1("", hash);
77
}
78
const blob = await readFile(filepath);
79
await callback2(save_blob, {
80
uuid,
81
blob,
82
ttl,
83
project_id,
84
database,
85
account_id,
86
});
87
} finally {
88
try {
89
await unlink(filepath);
90
} catch (err) {
91
dbg("WARNING -- failed to delete uploaded file", err);
92
}
93
}
94
}
95
if (!uuid) {
96
res.status(500).send("no file got uploaded");
97
return;
98
}
99
res.send({ uuid });
100
} catch (err) {
101
dbg("upload failed ", err);
102
res.status(500).send(`upload failed -- ${err}`);
103
}
104
});
105
}
106
107