aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/files-app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/files-app.js42
1 files changed, 39 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
index 4947f8c..65860ec 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
@@ -6,7 +6,7 @@ import { Icon } from './icon.js';
import { entriesUnder } from './zipstream.js';
import { transfers } from './transfers.js';
import {
- FILE_ICONS, formatSize, formatDate, canPreview, CHUNK_SIZE,
+ FILE_ICONS, formatSize, formatDate, canPreview, CHUNK_SIZE, MEMORY_CEILING,
pipelinedDownload, downloadEntry, downloadDirectory as sharedDownloadDirectory,
} from './file-utils.js';
@@ -66,7 +66,15 @@ function FilesPanel({
transport = transportRef.current;
gek = gekRef.current;
}
- if (!transport || !transport.connected) return;
+ // Never a silent return. A click that produces nothing at all — no
+ // transfer, no icon, no message — is indistinguishable from a broken
+ // button, and it is what a download looks like whenever the WebRTC
+ // connection is not up: on a screen lock, mid-reconnect, or after the node
+ // restarted. Say so instead.
+ if (!transport || !transport.connected) {
+ setError(t('group.download_offline'));
+ return;
+ }
await downloadEntry(transfers, transport, gek, entry);
}, [getTransport]);
@@ -86,13 +94,23 @@ function FilesPanel({
for (const file of files) {
transfers.start({
kind: 'upload', name: file.name, total: file.size, transport,
- run: async ({ signal, onProgress }) => {
+ // `makeLease`, not `lease`: pausing gives the slot back, so resuming
+ // has to be able to ask for another one, and a transfer handed a lease
+ // it cannot re-create is refused the button rather than offered one
+ // that would drop its slot for good.
+ makeLease: () => transport.openTransfer({ kind: 'upload',
+ bytes: file.size }),
+ // A `File` is seekable and the node remembers how much it holds, so
+ // there is no target tier to consult here — unlike a download.
+ pausable: true,
+ run: async ({ signal, onProgress, lease }) => {
await transport.uploadFile(file, {
// Bytes the node acknowledged, not bytes read locally.
onProgress: (sent) => onProgress(sent, file.size),
signal,
root: uploadRoot,
dir: uploadDir,
+ tr: lease && lease.tr,
});
// The node re-indexes on a filesystem event, so there is nothing to
// wait on but the clock. Refreshing here means the file appears in
@@ -598,6 +616,24 @@ function FilePreview({ entry, transportRef, gekRef, onClose, onDownload }) {
useEffect(() => {
let cancelled = false;
const load = async () => {
+ // Nothing here streams: a preview is decrypted whole, held as an array of
+ // chunks, and turned into a blob. That is right for a page of text and a
+ // photograph, and it is a dead tab for the things that also reach here —
+ // a scanned PDF, a multi-gigabyte .csv or .log. There was no size test at
+ // all, and the text branch is the sharpest illustration: it decoded the
+ // entire file and then kept 500 000 characters of it.
+ //
+ // Films and music never arrive (group-page.js's onPreview routes video to
+ // the MSE player and audio to the music queue), so this guard is only ever
+ // met by a document somebody clicked without knowing how big it was. It
+ // offers the download instead, which does stream.
+ if (entry.size > MEMORY_CEILING) {
+ setError(t('preview.too_large', {
+ size: formatSize(entry.size), limit: formatSize(MEMORY_CEILING),
+ }));
+ setPhase('error');
+ return;
+ }
const transport = transportRef.current;
if (!transport || !transport.connected) {
setError(t('video.err_transport'));