Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/archive/actions.ts
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 { webapp_client } from "@cocalc/frontend/webapp-client";
7
import {
8
filename_extension,
9
filename_extension_notilde,
10
keys,
11
path_split,
12
split,
13
} from "@cocalc/util/misc";
14
import { Actions, redux_name } from "../../app-framework";
15
import { register_file_editor } from "../../project-file";
16
import { Archive } from "./component";
17
import { COMMANDS, DOUBLE_EXT } from "./misc";
18
19
function init_redux(path: string, redux, project_id: string): string {
20
const name = redux_name(project_id, path);
21
if (redux.getActions(name) != null) {
22
return name;
23
}
24
redux.createStore(name);
25
const actions = redux.createActions(name, ArchiveActions);
26
actions.setArchiveContents(project_id, path);
27
return name;
28
}
29
30
function remove_redux(path: string, redux, project_id: string): string {
31
const name = redux_name(project_id, path);
32
redux.removeActions(name);
33
redux.removeStore(name);
34
return name;
35
}
36
37
interface State {
38
contents?: string;
39
type?: string;
40
loading?: boolean;
41
command?: string;
42
error?: string;
43
extract_output?: string;
44
}
45
46
export class ArchiveActions extends Actions<State> {
47
private project_id: string;
48
private path: string;
49
50
parse_file_type(file_info: string): string | undefined {
51
if (file_info.indexOf("Zip archive data") !== -1) {
52
return "zip";
53
} else if (file_info.indexOf("tar archive") !== -1) {
54
return "tar";
55
} else if (file_info.indexOf("gzip compressed data") !== -1) {
56
return "gz";
57
} else if (file_info.indexOf("bzip2 compressed data") !== -1) {
58
return "bzip2";
59
} else if (file_info.indexOf("lzip compressed data") !== -1) {
60
return "lzip";
61
} else if (file_info.indexOf("XZ compressed data") !== -1) {
62
return "xz";
63
}
64
return undefined;
65
}
66
67
setUnsupported(ext: string | undefined): void {
68
this.setState({
69
error: "unsupported",
70
contents: "",
71
type: ext,
72
});
73
}
74
75
/**
76
* Extract the extension, and check if there is a tilde.
77
*/
78
private extractExtension(pathReal: string): string | null {
79
const path = pathReal.toLowerCase(); // convert to lowercase for case-insensitive matching
80
const ext0 = filename_extension_notilde(path);
81
const ext = filename_extension(path);
82
if (ext0 !== ext) {
83
this.setState({
84
error: "Rename the archive file to not end in a tilde.",
85
});
86
return null;
87
}
88
// there are "double extension" with a dot, like "tar.bz2"
89
for (const ext of DOUBLE_EXT) {
90
if (path.endsWith(`.${ext}`)) {
91
return ext;
92
}
93
}
94
return ext;
95
}
96
97
private exec = async (opts) => {
98
const { project_id, path } = this;
99
const compute_server_id =
100
(await webapp_client.project_client.getServerIdForPath({
101
project_id,
102
path,
103
})) ?? 0;
104
return await webapp_client.exec({
105
filesystem: true,
106
compute_server_id,
107
project_id,
108
...opts,
109
});
110
};
111
112
async setArchiveContents(project_id: string, path: string): Promise<void> {
113
this.project_id = project_id;
114
this.path = path;
115
const ext = this.extractExtension(path);
116
if (ext === null) return;
117
118
if (COMMANDS[ext]?.list == null) {
119
this.setUnsupported(ext);
120
return;
121
}
122
123
const { command, args } = COMMANDS[ext].list;
124
125
try {
126
const output = await this.exec({
127
command,
128
args: args.concat([path]),
129
err_on_exit: true,
130
});
131
this.setState({
132
error: undefined,
133
contents: output?.stdout,
134
type: ext,
135
});
136
} catch (err) {
137
this.setState({
138
error: err?.toString(),
139
contents: undefined,
140
type: ext,
141
});
142
}
143
}
144
145
async extractArchiveFiles(
146
type: string | undefined,
147
contents: string | undefined,
148
): Promise<void> {
149
if (type == null || COMMANDS[type]?.extract == null) {
150
this.setUnsupported(type);
151
return;
152
}
153
let post_args;
154
let { command, args } = COMMANDS[type].extract;
155
const path_parts = path_split(this.path);
156
let extra_args: string[] = (post_args = []);
157
let output: any = undefined;
158
let base;
159
let error: string | undefined = undefined;
160
this.setState({ loading: true });
161
try {
162
if (contents == null) {
163
throw Error("Archive not loaded yet");
164
} else if (type === "zip") {
165
// special case for zip files: if heuristically it looks like not everything is contained
166
// in a subdirectory with name the zip file, then create that subdirectory.
167
base = path_parts.tail.slice(0, path_parts.tail.length - 4);
168
if (contents.indexOf(base + "/") === -1) {
169
extra_args = ["-d", base];
170
}
171
} else if (["tar", "tar.gz", "tar.bz2"].includes(type)) {
172
// special case for tar files: if heuristically it looks like not everything is contained
173
// in a subdirectory with name the tar file, then create that subdirectory.
174
const i = path_parts.tail.lastIndexOf(".t"); // hopefully that's good enough.
175
base = path_parts.tail.slice(0, i);
176
if (contents.indexOf(base + "/") === -1) {
177
post_args = ["-C", base];
178
await this.exec({
179
path: path_parts.head,
180
command: "mkdir",
181
args: ["-p", base],
182
error_on_exit: true,
183
});
184
}
185
}
186
args = args
187
.concat(extra_args != null ? extra_args : [])
188
.concat([path_parts.tail])
189
.concat(post_args);
190
const args_str = args
191
.map((x) => (x.indexOf(" ") !== -1 ? `'${x}'` : x))
192
.join(" ");
193
const cmd = `cd \"${path_parts.head}\" ; ${command} ${args_str}`; // ONLY for info purposes -- not actually run!
194
this.setState({ command: cmd });
195
output = await this.exec({
196
path: path_parts.head,
197
command,
198
args,
199
err_on_exit: true,
200
timeout: 120,
201
});
202
} catch (err) {
203
error = err.toString();
204
}
205
206
this.setState({
207
error,
208
extract_output: output?.stdout,
209
loading: false,
210
});
211
}
212
}
213
214
// TODO: change ext below to use keys(COMMANDS). We don't now, since there are a
215
// ton of extensions that should open in the archive editor, but aren't implemented
216
// yet and we don't want to open those in codemirror -- see https://github.com/sagemathinc/cocalc/issues/1720
217
// NOTE: One you implement one of these (so it is in commands), be
218
// sure to delete it from the list below.
219
const TODO_TYPES = split("z lz lzma tbz tb2 taz tz tlz txz");
220
register_file_editor({
221
ext: keys(COMMANDS).concat(TODO_TYPES),
222
icon: "file-archive",
223
init: init_redux,
224
remove: remove_redux,
225
component: Archive,
226
});
227
228