Skip to content

Commit

Permalink
Directory archive download in webR application
Browse files Browse the repository at this point in the history
  • Loading branch information
georgestagg committed Jun 24, 2024
1 parent 03b8972 commit c59cc9e
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function build(input: string, output: string, platform: esbuild.Platform, minify

const outputs = {
browser: [
build('repl/App.tsx', '../dist/repl.mjs', 'neutral', prod),
build('repl/App.tsx', '../dist/repl.mjs', 'browser', prod),
build('webR/chan/serviceworker.ts', '../dist/webr-serviceworker.js', 'browser', false),
build('webR/webr-worker.ts', '../dist/webr-worker.js', 'node', false),
build('webR/webr-main.ts', '../dist/webr.mjs', 'neutral', prod),
Expand Down
83 changes: 79 additions & 4 deletions src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"classnames": "^2.2.6",
"codemirror": "^6.0.1",
"codemirror-lang-r": "^0.1.0-2",
"jszip": "^3.10.1",
"lezer-r": "^0.1.1",
"lightningcss": "^1.21.5",
"prop-types": "^15.7.2",
Expand Down
56 changes: 46 additions & 10 deletions src/repl/components/Files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Panel } from 'react-resizable-panels';
import { WebR, WebRError } from '../../webR/webr-main';
import type { FSNode } from '../../webR/webr-main';
import { FilesInterface } from '../App';
import JSZip from 'jszip';
import './Files.css';

const FolderIcon = ({ isOpen }: { isOpen: boolean }) => isOpen
Expand Down Expand Up @@ -55,6 +56,31 @@ export function Files({
const uploadButtonRef = React.useRef<HTMLButtonElement | null>(null);
const downloadButtonRef = React.useRef<HTMLButtonElement | null>(null);

/* Take an `INode` selected from the tree in the UI and create a zip archive
* of the contents. Directories are added to the archive recursively.
*/
const zipFromFSNode = async (zip: JSZip, node: INode) => {
if (!zip || !node || !treeData) {
return;
}

if (node.children && node.children.length > 0) {
const dir = zip.folder(node.name);
await Promise.all(node.children.map((childId) => {
const child = treeData.find((value) => value.id === childId);
return zipFromFSNode(dir!, child!);
}));
} else {
const name = node.name;
const path = getNodePath(node);
await webR.FS.readFile(path).then((data) => {
zip.file(name, data, { binary: true });
}).catch((error: Error) => {
console.warn(`Problem encountered when creating archive: "${error.message}".`);
});
}
};

const nodeRenderer: ITreeViewProps['nodeRenderer'] = ({
element,
isExpanded,
Expand Down Expand Up @@ -109,20 +135,29 @@ export function Files({
};

const onDownload: React.MouseEventHandler<HTMLButtonElement> = () => {
if (!selectedNode) {
return;
}
const path = getNodePath(selectedNode);
void webR.FS.readFile(path).then((data) => {
const filename = selectedNode.name;
const doDownload = (filename: string, data: ArrayBufferView) => {
const blob = new Blob([data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = filename;
link.download = filename.replace(/[/\\<>:"|?*]/, '_');
link.href = url;
link.click();
link.remove();
});
};

if (!selectedNode) {
return;
} else if (isFileSelected) {
const path = getNodePath(selectedNode);
void webR.FS.readFile(path).then((data) => doDownload(selectedNode.name, data));
} else {
void (async () => {
const zip = new JSZip();
await zipFromFSNode(zip, selectedNode);
const data = await zip.generateAsync({type : "uint8array"});
doDownload(`${selectedNode.name}.zip`, data);
})();
}
};

const onOpen = async () => {
Expand Down Expand Up @@ -268,9 +303,10 @@ export function Files({
ref={downloadButtonRef}
onClick={onDownload}
className="download-file"
disabled={!selectedNode || !isFileSelected}
disabled={!selectedNode}
>
<Fa.FaFileDownload aria-hidden="true" className="icon" /> Download file
<Fa.FaFileDownload aria-hidden="true" className="icon" />
Download {isFileSelected ? "file" : "directory"}
</button>
<button
onClick={() => { void onOpen(); }}
Expand Down

0 comments on commit c59cc9e

Please sign in to comment.