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/project/read_write_files.ts
Views: 687
1
/*
2
* decaffeinate suggestions:
3
* DS102: Remove unnecessary code created because of implicit returns
4
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
5
*/
6
//########################################################################
7
// This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
8
// License: MS-RSL – see LICENSE.md for details
9
//########################################################################
10
11
import { CoCalcSocket } from "@cocalc/backend/tcp/enable-messaging-protocol";
12
import { execFile } from "node:child_process";
13
import { constants, Stats } from "node:fs";
14
import {
15
access,
16
readFile as readFileAsync,
17
stat as statAsync,
18
unlink,
19
writeFile,
20
} from "node:fs/promises";
21
import * as temp from "temp";
22
23
import ensureContainingDirectoryExists from "@cocalc/backend/misc/ensure-containing-directory-exists";
24
import { abspath, uuidsha1 } from "@cocalc/backend/misc_node";
25
import * as message from "@cocalc/util/message";
26
import { path_split } from "@cocalc/util/misc";
27
import { check_file_size } from "./common";
28
29
import { getLogger } from "@cocalc/backend/logger";
30
const winston = getLogger("read-write-files");
31
32
//##############################################
33
// Read and write individual files
34
//##############################################
35
36
// Read a file located in the given project. This will result in an
37
// error if the readFile function fails, e.g., if the file doesn't
38
// exist or the project is not open. We then send the resulting file
39
// over the socket as a blob message.
40
//
41
// Directories get sent as a ".tar.bz2" file.
42
// TODO: should support -- 'tar', 'tar.bz2', 'tar.gz', 'zip', '7z'. and mesg.archive option!!!
43
//
44
export async function read_file_from_project(socket: CoCalcSocket, mesg) {
45
const dbg = (...m) =>
46
winston.debug(`read_file_from_project(path='${mesg.path}'): `, ...m);
47
dbg("called");
48
let data: Buffer | undefined = undefined;
49
let path = abspath(mesg.path);
50
let is_dir: boolean | undefined = undefined;
51
let id: string | undefined = undefined;
52
let target: string | undefined = undefined;
53
let archive = undefined;
54
let stats: Stats | undefined = undefined;
55
56
try {
57
//dbg("Determine whether the path '#{path}' is a directory or file.")
58
stats = await statAsync(path);
59
is_dir = stats.isDirectory();
60
61
// make sure the file isn't too large
62
const size_check = check_file_size(stats.size);
63
if (size_check) {
64
throw new Error(size_check);
65
}
66
67
// tar jcf a directory
68
if (is_dir) {
69
if (mesg.archive !== "tar.bz2") {
70
throw new Error(
71
"The only supported directory archive format is tar.bz2"
72
);
73
}
74
target = temp.path({ suffix: "." + mesg.archive });
75
//dbg("'#{path}' is a directory, so archive it to '#{target}', change path, and read that file")
76
archive = mesg.archive;
77
if (path[path.length - 1] === "/") {
78
// common nuisance with paths to directories
79
path = path.slice(0, path.length - 1);
80
}
81
const split = path_split(path);
82
// TODO same patterns also in project.ts
83
const args = [
84
"--exclude=.sagemathcloud*",
85
"--exclude=.forever",
86
"--exclude=.node*",
87
"--exclude=.npm",
88
"--exclude=.sage",
89
"-jcf",
90
target as string,
91
split.tail,
92
];
93
//dbg("tar #{args.join(' ')}")
94
await new Promise<void>((resolve, reject) => {
95
execFile(
96
"tar",
97
args,
98
{ cwd: split.head },
99
function (err, stdout, stderr) {
100
if (err) {
101
winston.debug(
102
`Issue creating tarball: ${err}, ${stdout}, ${stderr}`
103
);
104
return reject(err);
105
} else {
106
return resolve();
107
}
108
}
109
);
110
});
111
} else {
112
//Nothing to do, it is a file.
113
target = path;
114
}
115
if (!target) {
116
throw Error("bug -- target must be set");
117
}
118
119
//dbg("Read the file into memory.")
120
data = await readFileAsync(target);
121
122
// get SHA1 of contents
123
if (data == null) {
124
throw new Error("data is null");
125
}
126
id = uuidsha1(data);
127
//dbg("sha1 hash = '#{id}'")
128
129
//dbg("send the file as a blob back to the hub.")
130
socket.write_mesg(
131
"json",
132
message.file_read_from_project({
133
id: mesg.id,
134
data_uuid: id,
135
archive,
136
})
137
);
138
139
socket.write_mesg("blob", {
140
uuid: id,
141
blob: data,
142
ttlSeconds: mesg.ttlSeconds, // TODO does ttlSeconds work?
143
});
144
} catch (err) {
145
if (err && err !== "file already known") {
146
socket.write_mesg(
147
"json",
148
message.error({ id: mesg.id, error: `${err}` })
149
);
150
}
151
}
152
153
// in any case, clean up the temporary archive
154
if (is_dir && target) {
155
try {
156
await access(target, constants.F_OK);
157
//dbg("It was a directory, so remove the temporary archive '#{path}'.")
158
await unlink(target);
159
} catch (err) {
160
winston.debug(`Error removing temporary archive '${target}': ${err}`);
161
}
162
}
163
}
164
165
export function write_file_to_project(socket: CoCalcSocket, mesg) {
166
const dbg = (...m) =>
167
winston.debug(`write_file_to_project(path='${mesg.path}'): `, ...m);
168
dbg("called");
169
170
const { data_uuid } = mesg;
171
const path = abspath(mesg.path);
172
173
// Listen for the blob containing the actual content that we will write.
174
const write_file = async function (type, value) {
175
if (type === "blob" && value.uuid === data_uuid) {
176
socket.removeListener("mesg", write_file);
177
try {
178
await ensureContainingDirectoryExists(path);
179
await writeFile(path, value.blob);
180
socket.write_mesg(
181
"json",
182
message.file_written_to_project({ id: mesg.id })
183
);
184
} catch (err) {
185
socket.write_mesg("json", message.error({ id: mesg.id, error: err }));
186
}
187
}
188
};
189
socket.on("mesg", write_file);
190
}
191
192