diff --git a/svelte/src/filesystem/Filesystem.svelte b/svelte/src/filesystem/Filesystem.svelte index 9cf8046..b100a92 100644 --- a/svelte/src/filesystem/Filesystem.svelte +++ b/svelte/src/filesystem/Filesystem.svelte @@ -5,7 +5,6 @@ import Toolbar from "./Toolbar.svelte"; import Breadcrumbs from "./Breadcrumbs.svelte"; import DetailsWindow from "./DetailsWindow.svelte"; import FilePreview from "./viewers/FilePreview.svelte"; -import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte"; import { global_navigator } from "./FSNavigator" import { css_from_path } from "filesystem/edit_window/Branding"; import { update_favicon } from "util/NovaLogo.svelte"; @@ -15,7 +14,6 @@ import ErrorPage from "./ErrorPage.svelte"; let file_preview: FilePreview = $state() let toolbar: Toolbar = $state() -let upload_widget: FSUploadWidget = $state() let details_visible = $state(false) let edit_window: EditWindow = $state() let edit_visible = $state(false) @@ -142,7 +140,6 @@ const keydown = (e: KeyboardEvent) => { @@ -165,10 +162,6 @@ const keydown = (e: KeyboardEvent) => { - - - diff --git a/svelte/src/filesystem/upload_widget/UploadFunc.ts b/svelte/src/filesystem/upload_widget/UploadFunc.ts deleted file mode 100644 index 4f26db2..0000000 --- a/svelte/src/filesystem/upload_widget/UploadFunc.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Uploads a file to the logged in user's Nova account. If no user is -// logged in the file is uploaded anonymously. -// -// on_progress reports progress on the file upload, parameter 1 is the uploaded -// file size and parameter 2 is the total file size -// -// on_success is called when the upload is done, the only parameter is the file -// ID -// -// on_error is called when the upload has failed. The parameters are the error - -import { fs_path_url, type GenericResponse } from "lib/FilesystemAPI.svelte" -import { get_user } from "lib/NovaAPI" - -// code and an error message -export const upload_file = async ( - file: Blob, - path: string, - on_progress: (loaded: number, total: number) => void, - on_success: (id: string) => void, - on_error: (code: string, message: string) => void, -) => { - const user = await get_user() - // Check the file size limit. For free accounts it's 20 GB - if (user !== null && user.subscription.file_size_limit === 0) { - user.subscription.file_size_limit = 20e9 - } - - if (file.size > user.subscription.file_size_limit) { - on_error( - "file_too_large", - "This file is too large. Check out the Pro subscription to increase the file size limit" - ) - return - } - - console.debug("Uploading file to", fs_path_url(path)) - - let xhr = new XMLHttpRequest(); - xhr.open("PUT", fs_path_url(path) + "?make_parents=true", true); - xhr.timeout = 86400000; // 24 hours, to account for slow connections - - xhr.upload.addEventListener("progress", evt => { - if (on_progress && evt.lengthComputable) { - on_progress(evt.loaded, evt.total) - } - }); - - xhr.onreadystatechange = () => { - // readystate 4 means the upload is done - if (xhr.readyState !== 4) { - return - } - - if (xhr.status >= 100 && xhr.status < 400) { - // Request is a success - on_success(JSON.parse(xhr.response).id) - } else if (xhr.status >= 400) { - // Request failed - console.log("Upload error. status: " + xhr.status + " response: " + xhr.response); - - let resp: GenericResponse - if (xhr.status === 429) { - resp = { - value: "too_many_requests", - message: "Too many requests. Please wait a few seconds", - } - } else { - resp = JSON.parse(xhr.response) as GenericResponse - } - - on_error(resp.value, resp.message) - } else if (xhr.status === 0) { - on_error("request_failed", "Your request did not arrive, check your network connection") - } else { - on_error(xhr.responseText, xhr.responseText) - } - }; - - xhr.send(file); - - return xhr -} diff --git a/svelte/src/filesystem/upload_widget/UploadProgress.svelte b/svelte/src/filesystem/upload_widget/UploadProgress.svelte deleted file mode 100644 index 8cfc1df..0000000 --- a/svelte/src/filesystem/upload_widget/UploadProgress.svelte +++ /dev/null @@ -1,105 +0,0 @@ - - -
-
- {job.file.name}
- {#if error_code !== ""} - {error_message}
- {error_code}
- {/if} - -
-
-
-
- - diff --git a/svelte/src/filesystem/viewers/FilePreview.svelte b/svelte/src/filesystem/viewers/FilePreview.svelte index 846849c..40c847d 100644 --- a/svelte/src/filesystem/viewers/FilePreview.svelte +++ b/svelte/src/filesystem/viewers/FilePreview.svelte @@ -14,18 +14,15 @@ import Torrent from "./Torrent.svelte"; import Zip from "./Zip.svelte"; import CustomBanner from "./CustomBanner.svelte"; import type { FSNavigator } from "filesystem/FSNavigator"; -import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte"; import EditWindow from "filesystem/edit_window/EditWindow.svelte"; import DetailsWindow from "filesystem/DetailsWindow.svelte"; let { nav, - upload_widget, edit_window, details_window, }: { nav: FSNavigator - upload_widget: FsUploadWidget edit_window: EditWindow details_window: DetailsWindow } = $props(); @@ -89,7 +86,7 @@ export const seek = (delta: number) => { {:else if viewer_type === "dir" && $nav.base.name === ".Trash"} {:else if viewer_type === "dir"} - + {:else if viewer_type === "audio"} diff --git a/svelte/src/home_page/HomePage.svelte b/svelte/src/home_page/HomePage.svelte index 7616542..57d239c 100644 --- a/svelte/src/home_page/HomePage.svelte +++ b/svelte/src/home_page/HomePage.svelte @@ -1,12 +1,9 @@
diff --git a/svelte/src/lib/DropTarget.ts b/svelte/src/lib/DropTarget.ts index 535dced..fd31d5c 100644 --- a/svelte/src/lib/DropTarget.ts +++ b/svelte/src/lib/DropTarget.ts @@ -1,7 +1,11 @@ +import type { Upload } from "filesystem/upload_widget/FSUploadWidget.svelte" + export const drop_target = ( node: HTMLElement, args: { - upload: (files: File[]) => void, + // Called as soon as the files are dropped, the promise resolves when + // all the dropped directories have been read + upload: (files: Promise) => void, shadow: string | undefined, }, ) => { @@ -39,7 +43,7 @@ export const drop_target = ( const dragleave = (e: DragEvent) => { node.style.boxShadow = "" } - const drop = async (e: DragEvent) => { + const drop = (e: DragEvent) => { node.style.boxShadow = "" if (can_upload(e)) { @@ -49,39 +53,59 @@ export const drop_target = ( return } - // if directory support is available - if (e.dataTransfer && e.dataTransfer.items && e.dataTransfer.items.length > 0) { - for (let i = 0; i < e.dataTransfer.items.length; i++) { - const entry: FileSystemEntry | null = e.dataTransfer.items[i].webkitGetAsEntry(); - if (entry !== null) { - read_dir_recursive(entry); - continue - } - const file: File | null = e.dataTransfer.items[i].getAsFile(); - if (file !== null) { - args.upload([file]); - continue - } + // The items are only valid until this function returns, so every entry + // has to be taken out of them before the first await + const files: Upload[] = [] + const reads: Promise[] = [] + for (let i = 0; i < e.dataTransfer.items.length; i++) { + const entry = e.dataTransfer.items[i].webkitGetAsEntry() + if (entry !== null) { + reads.push(read_entry(entry, files)) + continue } - } else if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) { - const files: File[] = [] - for (let i = 0; i < e.dataTransfer.files.length; i++) { - files.push(e.dataTransfer.files[i]) + + // Files which are not on the disk have no entry, like an image + // which is dragged out of another web page + const file = e.dataTransfer.items[i].getAsFile() + if (file !== null) { + files.push({ file, path: file.name }) } - args.upload(files) } + args.upload(Promise.all(reads).then(() => files)) } - const read_dir_recursive = (item: FileSystemEntry) => { - if (item.isDirectory) { - (item as FileSystemDirectoryEntry).createReader().readEntries(entries => { - entries.forEach(entry => { - read_dir_recursive(entry); - }); - }); - } else { - (item as FileSystemFileEntry).file(file => { - args.upload([file]) - }); + + // Adds a file, or all the files in a directory, to the list. The path comes + // from the entry. Firefox also puts it in the file's webkitRelativePath, + // but Chromium leaves that empty for dropped files + const read_entry = async (entry: FileSystemEntry, files: Upload[]) => { + if (entry.isFile) { + files.push({ + path: entry.fullPath.slice(1), + file: await new Promise((resolve, reject) => + (entry as FileSystemFileEntry).file(resolve, reject) + ), + }) + return + } + + // A reader returns the directory in parts, Chromium gives 100 entries + // at a time. An empty part marks the end + const reader = (entry as FileSystemDirectoryEntry).createReader() + while (true) { + const entries = await new Promise( + (resolve, reject) => reader.readEntries(resolve, reject) + ) + if (entries.length === 0) { + return + } + + // One at a time on purpose. Chromium asks another process for + // each file, and handles all the answers which have arrived in + // one go. Asking for a whole tree at once is a third faster, but + // freezes the page for seconds: 7 for 100k files + for (const e of entries) { + await read_entry(e, files) + } } } diff --git a/svelte/src/wrap/App.svelte b/svelte/src/wrap/App.svelte index 815a650..6000c36 100644 --- a/svelte/src/wrap/App.svelte +++ b/svelte/src/wrap/App.svelte @@ -17,6 +17,7 @@ import { breadcrumbs_store, toolbar_store } from "./HeaderStore"; import ProgressBar from "util/ProgressBar.svelte"; import NovaLogo, { update_favicon } from "util/NovaLogo.svelte"; import SearchBar from "filesystem/SearchBar.svelte"; +import FSUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte"; // The menu swipe will be detected if it was less than this much pixels from the // screen edge @@ -320,6 +321,7 @@ const set_offset = (off: number) => { +