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.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/hub/servers/app/blob-upload.ts
Views: 687
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 (!project_id || typeof project_id != "string") {
38
res.status(500).send("project_id must be specified");
39
return;
40
}
41
if (!(await isCollaborator({ account_id, project_id }))) {
42
res.status(500).send("user must be collaborator on project");
43
return;
44
}
45
46
dbg({ account_id, project_id });
47
48
// TODO: check for throttling/limits
49
try {
50
const form = formidable({
51
keepExtensions: true,
52
maxFileSize: MAX_BLOB_SIZE,
53
hashAlgorithm: "sha1",
54
});
55
56
dbg("parsing form data...");
57
// https://github.com/node-formidable/formidable?tab=readme-ov-file#parserequest-callback
58
const [_, files] = await form.parse(req);
59
//dbg(`finished parsing form data. ${JSON.stringify({ fields, files })}`);
60
61
/* Just for the sake of understanding this, this is how this looks like in the real world (formidable@3):
62
> files.file[0]
63
{
64
size: 80789,
65
filepath: '/home/hsy/p/cocalc/src/data/projects/c8787b71-a85f-437b-9d1b-29833c3a199e/asdf/asdf/8e3e4367333e45275a8d1aa03.png',
66
newFilename: '8e3e4367333e45275a8d1aa03.png',
67
mimetype: 'application/octet-stream',
68
mtime: '2024-04-23T09:25:53.197Z',
69
originalFilename: 'Screenshot from 2024-04-23 09-20-40.png'
70
}
71
*/
72
let uuid: string | undefined = undefined;
73
if (files.file?.[0] != null) {
74
const { filepath, hash } = files.file[0];
75
try {
76
dbg("got", files);
77
if (typeof hash == "string") {
78
uuid = uuidsha1("", hash);
79
}
80
const blob = await readFile(filepath);
81
await callback2(save_blob, {
82
uuid,
83
blob,
84
ttl,
85
project_id,
86
database,
87
});
88
} finally {
89
try {
90
await unlink(filepath);
91
} catch (err) {
92
dbg("WARNING -- failed to delete uploaded file", err);
93
}
94
}
95
}
96
if (!uuid) {
97
res.status(500).send("no file got uploaded");
98
return;
99
}
100
res.send({ uuid });
101
} catch (err) {
102
dbg("upload failed ", err);
103
res.status(500).send(`upload failed -- ${err}`);
104
}
105
});
106
}
107
108