Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/unknown/editor.tsx
1691 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { React, useActions, useTypedRedux, CSS } from "../../app-framework";
7
import { delay } from "awaiting";
8
import { webapp_client } from "../../webapp-client";
9
import { Button, Alert, Typography, Row, Col } from "antd";
10
const { Text } = Typography;
11
import { register_file_editor } from "../../frame-editors/frame-tree/register";
12
import { filename_extension_notilde } from "@cocalc/util/misc";
13
import { Loading } from "../../components";
14
import { Editor as CodeEditor } from "../../frame-editors/code-editor/editor";
15
import { Actions as CodeEditorActions } from "../../frame-editors/code-editor/actions";
16
17
const STYLE: CSS = {
18
margin: "0 auto",
19
padding: "20px",
20
maxWidth: "1000px",
21
};
22
23
interface Props {
24
path: string;
25
project_id: string;
26
}
27
28
async function get_mime({ project_id, path, set_mime, set_err, set_snippet }) {
29
try {
30
let mime = "";
31
const compute_server_id =
32
(await webapp_client.project_client.getServerIdForPath({
33
project_id,
34
path,
35
})) ?? 0;
36
const { stdout: mime_raw, exit_code: exit_code1 } =
37
await webapp_client.project_client.exec({
38
project_id,
39
command: "file",
40
args: ["-b", "--mime-type", path],
41
compute_server_id,
42
filesystem: true,
43
});
44
if (exit_code1 != 0) {
45
set_err(`Error: exit_code1 = ${exit_code1}`);
46
} else {
47
mime = mime_raw.split("\n")[0].trim();
48
set_mime(mime);
49
}
50
51
const is_binary = !mime.startsWith("text/");
52
// limit number of bytes – it could be a "one-line" monster file.
53
// We *ONLY* limit by number of bytes, because limiting by both
54
// bytes and lines isn't supported in POSIX (e.g., macos), even though
55
// it works in Linux.
56
const content_cmd = is_binary
57
? {
58
command: "hexdump",
59
args: ["-C", "-n", "512", path],
60
}
61
: {
62
command: "head",
63
args: ["-c", "2000", path],
64
};
65
66
const { stdout: raw, exit_code: exit_code2 } =
67
await webapp_client.project_client.exec({
68
project_id,
69
...content_cmd,
70
compute_server_id,
71
filesystem: true,
72
});
73
if (exit_code2 != 0) {
74
set_err(`Error: exit_code2 = ${exit_code2}`);
75
} else {
76
if (is_binary) {
77
set_snippet(raw);
78
} else {
79
set_snippet(
80
// 80 char line break and limit overall length
81
raw
82
.trim()
83
.slice(0, 20 * 80)
84
.split(/(.{0,80})/g)
85
.filter((x) => !!x)
86
.join(""),
87
);
88
}
89
}
90
} catch (err) {
91
set_err(err.toString());
92
}
93
}
94
95
export const UnknownEditor: React.FC<Props> = (props: Props) => {
96
const { path, project_id } = props;
97
const ext = filename_extension_notilde(path).toLowerCase();
98
const NAME = useTypedRedux("customize", "site_name");
99
const actions = useActions({ project_id });
100
const [mime, set_mime] = React.useState("");
101
const [err, set_err] = React.useState("");
102
const [snippet, set_snippet] = React.useState("");
103
104
React.useEffect(() => {
105
if (mime) return;
106
get_mime({ project_id, path, set_mime, set_err, set_snippet });
107
}, []);
108
109
React.useEffect(() => {
110
if (actions == null) return;
111
switch (mime) {
112
case "inode/directory":
113
(async () => {
114
// It is actually a directory so we know what to do.
115
// See https://github.com/sagemathinc/cocalc/issues/5212
116
actions.open_directory(path);
117
// We delay before closing this file, since closing the file causes a
118
// state change to this component at the same time
119
// as updating (which is a WARNING).
120
await delay(1);
121
actions.close_file(path);
122
})();
123
break;
124
case "text/plain":
125
if (ext) {
126
// automatically register the code editor
127
register_file_editor({
128
ext: [ext],
129
component: CodeEditor,
130
Actions: CodeEditorActions,
131
});
132
(async () => {
133
actions.close_file(path);
134
await delay(1);
135
actions.open_file({ path });
136
})();
137
}
138
break;
139
}
140
}, [mime]);
141
142
function render_ext() {
143
return (
144
<Text strong>
145
<Text code>*.{ext}</Text>
146
</Text>
147
);
148
}
149
150
const explanation = React.useMemo(() => {
151
if (mime == "inode/x-empty") {
152
return (
153
<span>
154
This file is empty and has the unknown file-extension: {render_ext()}.
155
</span>
156
);
157
} else if (mime.startsWith("text/")) {
158
return (
159
<span>
160
This file might contain plain text, but the file-extension:{" "}
161
{render_ext()}
162
is unknown. Try the Code Editor!
163
</span>
164
);
165
} else {
166
return (
167
<span>
168
This is likely a binary file and the file-extension: {render_ext()} is
169
unknown. Preferrably, you have to open this file via a library/package
170
in a programming environment, like a Jupyter Notebook.
171
</span>
172
);
173
}
174
}, [mime]);
175
176
async function register(ext, editor: "code") {
177
switch (editor) {
178
case "code":
179
register_file_editor({
180
ext: [ext],
181
component: CodeEditor,
182
Actions: CodeEditorActions,
183
});
184
break;
185
default:
186
console.warn(`Unknown editor of type ${editor}, aborting.`);
187
return;
188
}
189
if (actions == null) {
190
console.warn(
191
`Project Actions for ${project_id} not available – shouldn't happen.`,
192
);
193
return;
194
}
195
actions.close_file(path);
196
await delay(1);
197
actions.open_file({ path });
198
}
199
200
function render_header() {
201
return <h1>Unknown file extension</h1>;
202
}
203
204
function render_info() {
205
return (
206
<div>
207
{NAME} does not know what to do with this file with the extension{" "}
208
{render_ext()}. For this session, you can register one of the editors to
209
open up this file.
210
</div>
211
);
212
}
213
214
function render_warning() {
215
if (mime.startsWith("text/")) return;
216
return (
217
<Alert
218
message="Warning"
219
description="Opening binary files could possibly modify and hence damage them. If this happens, you can use Files → Backup to restore them."
220
type="warning"
221
showIcon
222
/>
223
);
224
}
225
226
function render_register() {
227
return (
228
<>
229
<div>
230
{NAME} detected that the file's content has the MIME code{" "}
231
<Text strong>
232
<Text code>{mime}</Text>
233
</Text>
234
. {explanation}
235
</div>
236
<div>The following editors are available:</div>
237
<ul>
238
<li>
239
<Button onClick={() => register(ext, "code")}>
240
Open {render_ext()} using <Text code>Code Editor</Text>
241
</Button>
242
</li>
243
</ul>
244
<div>
245
<Text type="secondary">
246
<Text strong>Note:</Text> by clicking this button this file will
247
open immediately. This will be remembered until you open up {NAME}{" "}
248
again or refresh this page. Alternatively, rename this file's file
249
extension.
250
</Text>
251
</div>
252
</>
253
);
254
}
255
256
function render_content() {
257
if (!snippet) return;
258
return (
259
<>
260
<div>The content of this file starts like that:</div>
261
<div>
262
<pre style={{ fontSize: "70%" }}>{snippet}</pre>
263
</div>
264
</>
265
);
266
}
267
268
function render() {
269
if (!mime) {
270
return <Loading theme={"medium"} />;
271
}
272
return (
273
<>
274
<Col flex={1}>{render_header()}</Col>
275
<Col flex={1}>{render_info()}</Col>
276
<Col flex={1}>{render_warning()}</Col>
277
<Col flex={1}>{render_register()}</Col>
278
<Col flex={1}>{render_content()}</Col>
279
</>
280
);
281
}
282
283
return (
284
<div style={{ overflow: "auto" }}>
285
<div style={STYLE}>
286
{err ? (
287
<Alert type="error" message="Error" showIcon description={err} />
288
) : (
289
<Row gutter={[24, 24]}>{render()}</Row>
290
)}
291
</div>
292
</div>
293
);
294
};
295
296