Skip to content

Commit 2cccb72

Browse files
authored
Merge pull request #24 from fcollonval/submenu
Use a submenu if archive format is null
2 parents 3059414 + 5afc5a9 commit 2cccb72

File tree

3 files changed

+170
-49
lines changed

3 files changed

+170
-49
lines changed

jupyter_archive/handlers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
# The delay in ms at which we send the chunk of data
1313
# to the client.
1414
ARCHIVE_DOWNLOAD_FLUSH_DELAY = 100
15+
SUPPORTED_FORMAT = [
16+
"zip",
17+
"tgz",
18+
"tar.gz",
19+
"tbz",
20+
"tbz2",
21+
"tar.bz",
22+
"tar.bz2",
23+
"txz",
24+
"tar.xz"
25+
]
1526

1627

1728
class ArchiveStream():
@@ -84,6 +95,9 @@ def get(self, archive_path, include_body=False):
8495

8596
archive_token = self.get_argument('archiveToken')
8697
archive_format = self.get_argument('archiveFormat', 'zip')
98+
if archive_format not in SUPPORTED_FORMAT:
99+
self.log.error("Unsupported format {}.".format(archive_format))
100+
raise web.HTTPError(404)
87101

88102
archive_path = os.path.join(cm.root_dir, url2path(archive_path))
89103

schema/archive.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"description": "Archive handler options.",
44
"properties": {
55
"format": {
6-
"type": "string",
7-
"enum": ["zip", "tgz", "tar.gz","tbz", "tbz2", "tar.bz", "tar.bz2","txz", "tar.xz"],
6+
"type": ["string", "null"],
7+
"enum": [null, "zip", "tgz", "tar.gz","tbz", "tbz2", "tar.bz", "tar.bz2","txz", "tar.xz"],
88
"title": "Archive format",
9-
"description": "Archive format for compressing folder; one of ['zip', 'tgz', 'tar.gz', 'tbz', 'tbz2', 'tar.bz', 'tar.bz2', 'txz', 'tar.xz']",
9+
"description": "Archive format for compressing folder; one of [null (submenu), 'zip', 'tgz', 'tar.gz', 'tbz', 'tbz2', 'tar.bz', 'tar.bz2', 'txz', 'tar.xz']",
1010
"default": "zip"
1111
}
1212
},

src/index.ts

Lines changed: 153 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,38 @@ import {
22
JupyterFrontEnd,
33
JupyterFrontEndPlugin
44
} from "@jupyterlab/application";
5-
6-
import { each } from "@phosphor/algorithm";
5+
import { showErrorMessage } from "@jupyterlab/apputils";
6+
import { ISettingRegistry, URLExt } from "@jupyterlab/coreutils";
77
import { IFileBrowserFactory } from "@jupyterlab/filebrowser";
88
import { ServerConnection } from "@jupyterlab/services";
9-
import { URLExt, ISettingRegistry } from "@jupyterlab/coreutils";
10-
import { showErrorMessage } from "@jupyterlab/apputils";
9+
import { each } from "@phosphor/algorithm";
10+
import { IDisposable } from "@phosphor/disposable";
11+
import { Menu } from "@phosphor/widgets";
1112

1213
const DIRECTORIES_URL = "directories";
1314
const EXTRACT_ARCHVE_URL = "extract-archive";
15+
type ArchiveFormat =
16+
| null
17+
| "zip"
18+
| "tgz"
19+
| "tar.gz"
20+
| "tbz"
21+
| "tbz2"
22+
| "tar.bz"
23+
| "tar.bz2"
24+
| "txz"
25+
| "tar.xz";
1426

1527
namespace CommandIDs {
1628
export const downloadArchive = "filebrowser:download-archive";
1729
export const extractArchive = "filebrowser:extract-archive";
18-
export const downloadArchiveCurrentFolder = "filebrowser:download-archive-current-folder";
30+
export const downloadArchiveCurrentFolder =
31+
"filebrowser:download-archive-current-folder";
1932
}
2033

2134
function downloadArchiveRequest(
2235
path: string,
23-
archiveFormat: string
36+
archiveFormat: ArchiveFormat
2437
): Promise<void> {
2538
const settings = ServerConnection.makeSettings();
2639

@@ -56,8 +69,8 @@ function downloadArchiveRequest(
5669
} else {
5770
let element = document.createElement("a");
5871
document.body.appendChild(element);
59-
element.setAttribute('href', url);
60-
element.setAttribute('download', '');
72+
element.setAttribute("href", url);
73+
element.setAttribute("download", "");
6174
element.click();
6275
document.body.removeChild(element);
6376
}
@@ -107,54 +120,145 @@ const extension: JupyterFrontEndPlugin<void> = {
107120
const { commands } = app;
108121
const { tracker } = factory;
109122

110-
let archiveFormat: string = "zip";
123+
const allowedArchiveExtensions = [
124+
".zip",
125+
".tgz",
126+
".tar.gz",
127+
".tbz",
128+
".tbz2",
129+
".tar.bz",
130+
".tar.bz2",
131+
".txz",
132+
".tar.xz"
133+
];
134+
let archiveFormat: ArchiveFormat; // Default value read from settings
135+
136+
// matches anywhere on filebrowser
137+
const selectorContent = ".jp-DirListing-content";
138+
139+
// matches all filebrowser items
140+
const selectorOnlyDir = '.jp-DirListing-item[data-isdir="true"]';
141+
142+
// Create submenus
143+
const archiveFolder = new Menu({
144+
commands
145+
});
146+
archiveFolder.title.label = "Download As";
147+
archiveFolder.title.iconClass = "jp-MaterialIcon jp-DownloadIcon";
148+
const archiveCurrentFolder = new Menu({
149+
commands
150+
});
151+
archiveCurrentFolder.title.label = "Download Current Folder As";
152+
archiveCurrentFolder.title.iconClass = "jp-MaterialIcon jp-DownloadIcon";
153+
154+
["zip", "tar.bz2", "tar.gz", "tar.xz"].forEach(format => {
155+
archiveFolder.addItem({
156+
command: CommandIDs.downloadArchive,
157+
args: { format }
158+
});
159+
archiveCurrentFolder.addItem({
160+
command: CommandIDs.downloadArchiveCurrentFolder,
161+
args: { format }
162+
});
163+
});
164+
165+
// Reference to menu items
166+
let archiveFolderItem: IDisposable;
167+
let archiveCurrentFolderItem: IDisposable;
168+
169+
function updateFormat(newFormat: ArchiveFormat, oldFormat: ArchiveFormat) {
170+
if (newFormat !== oldFormat) {
171+
if (
172+
newFormat === null ||
173+
oldFormat === null ||
174+
oldFormat === undefined
175+
) {
176+
if (oldFormat !== undefined) {
177+
archiveFolderItem.dispose();
178+
archiveCurrentFolderItem.dispose();
179+
}
180+
181+
if (newFormat === null) {
182+
archiveFolderItem = app.contextMenu.addItem({
183+
selector: selectorOnlyDir,
184+
rank: 10,
185+
type: "submenu",
186+
submenu: archiveFolder
187+
});
188+
189+
archiveCurrentFolderItem = app.contextMenu.addItem({
190+
selector: selectorContent,
191+
rank: 3,
192+
type: "submenu",
193+
submenu: archiveCurrentFolder
194+
});
195+
} else {
196+
archiveFolderItem = app.contextMenu.addItem({
197+
command: CommandIDs.downloadArchive,
198+
selector: selectorOnlyDir,
199+
rank: 10
200+
});
201+
202+
archiveCurrentFolderItem = app.contextMenu.addItem({
203+
command: CommandIDs.downloadArchiveCurrentFolder,
204+
selector: selectorContent,
205+
rank: 3
206+
});
207+
}
208+
}
209+
210+
archiveFormat = newFormat;
211+
}
212+
}
111213

112214
// Load the settings
113215
settingRegistry
114216
.load("@hadim/jupyter-archive:archive")
115217
.then(settings => {
116218
settings.changed.connect(settings => {
117-
archiveFormat = settings.get("format").composite as string;
219+
const newFormat = settings.get("format").composite as ArchiveFormat;
220+
updateFormat(newFormat, archiveFormat);
118221
});
119-
archiveFormat = settings.get("format").composite as string;
222+
223+
const newFormat = settings.get("format").composite as ArchiveFormat;
224+
updateFormat(newFormat, archiveFormat);
120225
})
121226
.catch(reason => {
122227
console.error(reason);
123228
showErrorMessage(
124-
"Fail to read settings for '@jupyterlab/archive:archive'",
229+
"Fail to read settings for '@hadim/jupyter-archive:archive'",
125230
reason
126231
);
127232
});
128233

129-
// matches anywhere on filebrowser
130-
const selectorContent = '.jp-DirListing-content';
131-
132-
// matches all filebrowser items
133-
const selectorOnlyDir = '.jp-DirListing-item[data-isdir="true"]';
134-
135-
// Add the 'download_archive' command to the file's menu.
234+
// Add the 'downloadArchive' command to the file's menu.
136235
commands.addCommand(CommandIDs.downloadArchive, {
137-
execute: () => {
236+
execute: args => {
138237
const widget = tracker.currentWidget;
139238
if (widget) {
140239
each(widget.selectedItems(), item => {
141240
if (item.type == "directory") {
142-
downloadArchiveRequest(item.path, archiveFormat);
241+
const format = args["format"] as ArchiveFormat;
242+
downloadArchiveRequest(
243+
item.path,
244+
allowedArchiveExtensions.indexOf("." + format) >= 0
245+
? format
246+
: archiveFormat
247+
);
143248
}
144249
});
145250
}
146251
},
147-
iconClass: "jp-MaterialIcon jp-DownloadIcon",
148-
label: "Download as an archive"
149-
});
150-
151-
app.contextMenu.addItem({
152-
command: CommandIDs.downloadArchive,
153-
selector: selectorOnlyDir,
154-
rank: 10
252+
iconClass: args =>
253+
"format" in args ? "" : "jp-MaterialIcon jp-DownloadIcon",
254+
label: args => {
255+
const format = (args["format"] as ArchiveFormat) || "";
256+
const label = format.replace(".", " ").toLocaleUpperCase();
257+
return label ? `${label} Archive` : "Download as an Archive";
258+
}
155259
});
156260

157-
// Add the 'extract_archive' command to the file's menu.
261+
// Add the 'extractArchive' command to the file's menu.
158262
commands.addCommand(CommandIDs.extractArchive, {
159263
execute: () => {
160264
const widget = tracker.currentWidget;
@@ -165,14 +269,11 @@ const extension: JupyterFrontEndPlugin<void> = {
165269
}
166270
},
167271
iconClass: "jp-MaterialIcon jp-DownCaretIcon",
168-
label: "Extract archive"
272+
label: "Extract Archive"
169273
});
170274

171275
// Add a command for each archive extensions
172276
// TODO: use only one command and accept multiple extensions.
173-
const allowedArchiveExtensions = [".zip", ".tgz", ".tar.gz",".tbz", ".tbz2",
174-
".tar.bz", ".tar.bz2", ".txz", ".tar.xz"]
175-
176277
allowedArchiveExtensions.forEach(extension => {
177278
const selector = '.jp-DirListing-item[title$="' + extension + '"]';
178279
app.contextMenu.addItem({
@@ -182,24 +283,30 @@ const extension: JupyterFrontEndPlugin<void> = {
182283
});
183284
});
184285

185-
// Add the 'download_archive' command to fiel browser content.
286+
// Add the 'downloadArchiveCurrentFolder' command to file browser content.
186287
commands.addCommand(CommandIDs.downloadArchiveCurrentFolder, {
187-
execute: () => {
288+
execute: args => {
188289
const widget = tracker.currentWidget;
189290
if (widget) {
190-
downloadArchiveRequest(widget.model.path, archiveFormat);
291+
const format = args["format"] as ArchiveFormat;
292+
downloadArchiveRequest(
293+
widget.model.path,
294+
allowedArchiveExtensions.indexOf("." + format) >= 0
295+
? format
296+
: archiveFormat
297+
);
191298
}
192299
},
193-
iconClass: "jp-MaterialIcon jp-DownloadIcon",
194-
label: "Download current folder as an archive"
195-
});
196-
197-
app.contextMenu.addItem({
198-
command: CommandIDs.downloadArchiveCurrentFolder,
199-
selector: selectorContent,
200-
rank: 3
300+
iconClass: args =>
301+
"format" in args ? "" : "jp-MaterialIcon jp-DownloadIcon",
302+
label: args => {
303+
const format = (args["format"] as ArchiveFormat) || "";
304+
const label = format.replace(".", " ").toLocaleUpperCase();
305+
return label
306+
? `${label} Archive`
307+
: "Download Current Folder as an Archive";
308+
}
201309
});
202-
203310
}
204311
};
205312

0 commit comments

Comments
 (0)