-
Notifications
You must be signed in to change notification settings - Fork 300
feat(filemanager): add download all button and 4GB archive size warning/disable #333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
c59d66f
070624f
50b55ce
2d90662
1ed8b5f
59d8291
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,26 @@ | ||
| import { Stack, useMediaQuery, useTheme } from "@mui/material"; | ||
| import Breadcrumb from "./Breadcrumb.tsx"; | ||
| import TopActions from "./TopActions.tsx"; | ||
| import { useContext } from "react"; | ||
| import { useTranslation } from "react-i18next"; | ||
| import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; | ||
| import { downloadAllFiles } from "../../../redux/thunks/download.ts"; | ||
| import { Filesystem } from "../../../util/uri.ts"; | ||
| import { RadiusFrame } from "../../Frame/RadiusFrame.tsx"; | ||
| import TopActionsSecondary from "./TopActionsSecondary.tsx"; | ||
| import Download from "../../Icons/Download.tsx"; | ||
| import { FmIndexContext } from "../FmIndexContext.tsx"; | ||
| import { SearchIndicator } from "../Search/SearchIndicator.tsx"; | ||
| import Breadcrumb from "./Breadcrumb.tsx"; | ||
| import TopActions, { ActionButton, ActionButtonGroup } from "./TopActions.tsx"; | ||
| import TopActionsSecondary from "./TopActionsSecondary.tsx"; | ||
|
|
||
| const NavHeader = () => { | ||
| const theme = useTheme(); | ||
| const { t } = useTranslation(); | ||
| const dispatch = useAppDispatch(); | ||
| const isMobile = useMediaQuery(theme.breakpoints.down("sm")); | ||
| const fmIndex = useContext(FmIndexContext); | ||
| const fs = useAppSelector((state) => state.fileManager[fmIndex].current_fs); | ||
| const isSingleFileView = useAppSelector((state) => state.fileManager[fmIndex].list?.single_file_view); | ||
| const showDownloadFolder = fs == Filesystem.share && !isSingleFileView; | ||
| return ( | ||
| <Stack | ||
| direction={"row"} | ||
|
|
@@ -36,6 +49,19 @@ const NavHeader = () => { | |
| <RadiusFrame> | ||
| <TopActions /> | ||
| </RadiusFrame> | ||
| {showDownloadFolder && ( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This existing download works for selecting a folder from the parent, but if I'm inside a folder or shared folder there is no way to download all files. "Select all" only selects the current page due to pagination. For a client, or a recipient of a link, I think it's important to have a one click 'download all' or 'download folder' button, especially in cases when there are lots of files being sent (for example a photo gallery from a job with 500 photos in it etc). Similar services like frame.io and Dropbox have this feature for share pages specifically as a client or customer receiving a link they need an option to be able to download all the contents of the folder with one click.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have made some tweaks to the UI/UX for this section, let me know if you like any of this flow Mobile layout: The "Download folder" button now shows icon-only on mobile and text + icon on desktop, with a small animation. No duplicate buttons: The download folder button automatically hides when files are selected, since the navbar's FileSelectedActions already provides a download button for selections (on mobile) File summary in archive method dialog: The "Select archive method" dialog now shows a subtitle with the file/folder count and total size (e.g. "2 files · 4.5 GB"), giving users context about what they're downloading before they pick a method. This is a generic enhancement to SelectOption - added optional subtitle support that any dialog can use.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| <RadiusFrame> | ||
| <ActionButtonGroup variant="outlined"> | ||
| <ActionButton | ||
| startIcon={<Download />} | ||
| onClick={() => dispatch(downloadAllFiles(fmIndex))} | ||
| sx={{ color: "primary.main" }} | ||
| > | ||
| {t("application:fileManager.downloadFolder")} | ||
| </ActionButton> | ||
| </ActionButtonGroup> | ||
| </RadiusFrame> | ||
| )} | ||
| </Stack> | ||
| ); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,8 @@ import dayjs from "dayjs"; | |
| import i18next from "i18next"; | ||
| import { closeSnackbar, enqueueSnackbar } from "notistack"; | ||
| import streamSaver from "streamsaver"; | ||
| import { getFileEntityUrl } from "../../api/api.ts"; | ||
| import { FileResponse, FileType, Metadata } from "../../api/explorer.ts"; | ||
| import { getFileEntityUrl, getFileList } from "../../api/api.ts"; | ||
| import { FileResponse, FileType, ListResponse, Metadata } from "../../api/explorer.ts"; | ||
| import { GroupPermission } from "../../api/user.ts"; | ||
| import { | ||
| DefaultCloseAction, | ||
|
|
@@ -12,7 +12,7 @@ import { | |
| BatchDownloadCompleteAction, | ||
| } from "../../component/Common/Snackbar/snackbar.tsx"; | ||
| import SessionManager from "../../session"; | ||
| import { getFileLinkedUri } from "../../util"; | ||
| import { getFileLinkedUri, sizeToString } from "../../util"; | ||
| import Boolset from "../../util/boolset.ts"; | ||
| import { formatLocalTime } from "../../util/datetime.ts"; | ||
| import { | ||
|
|
@@ -25,6 +25,7 @@ import { closeContextMenu } from "../fileManagerSlice.ts"; | |
| import { DialogSelectOption, setBatchDownloadLog, setBatchDownloadProgress } from "../globalStateSlice.ts"; | ||
| import { AppThunk } from "../store.ts"; | ||
| import { promiseId, selectOption } from "./dialog.ts"; | ||
| import { MinPageSize } from "../../component/FileManager/TopBar/ViewOptionPopover.tsx"; | ||
| import { longRunningTaskWithSnackbar, refreshSingleFileSymbolicLinks, walk, walkAll } from "./file.ts"; | ||
|
|
||
| enum MultipleDownloadOption { | ||
|
|
@@ -51,6 +52,54 @@ export function downloadFiles(index: number, files: FileResponse[]): AppThunk { | |
| }; | ||
| } | ||
|
|
||
| export function downloadAllFiles(index: number): AppThunk { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can make a small change in backend to allow calculate total size on root file
And then we can calculate the file size on server side and re-use the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, cleaner approach. want me to wait for the backend change, or is the current frontend pagination approach OK for now?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, I have raise a new pull request to backend, you may remove the pagination approach after it is merged.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can update to use this once merged |
||
| return async (dispatch, getState) => { | ||
| const fm = getState().fileManager[index]; | ||
| const uri = fm.pure_path; | ||
| if (!uri) { | ||
| return; | ||
| } | ||
|
|
||
| // Fetch all files in the current folder, bypassing pagination | ||
| const allFiles: FileResponse[] = []; | ||
| let nextToken: string | undefined = undefined; | ||
| let page: number | undefined = undefined; | ||
| while (true) { | ||
| const res: ListResponse = await dispatch( | ||
| getFileList({ | ||
| uri, | ||
| next_page_token: nextToken, | ||
| page, | ||
| page_size: 1000, | ||
| }), | ||
| ); | ||
| allFiles.push(...res.files); | ||
| if (res.pagination.total_items) { | ||
| page = (page ?? 0) + 1; | ||
| } else if (res.pagination.next_token) { | ||
| nextToken = res.pagination.next_token; | ||
| } | ||
|
|
||
| const pageSize = res.pagination?.page_size; | ||
| const totalPages = Math.ceil( | ||
| (res.pagination.total_items ?? 1) / (pageSize && pageSize > 0 ? pageSize : MinPageSize), | ||
| ); | ||
| const usePagination = totalPages > 1; | ||
| const loadMore = nextToken || (usePagination && (page ?? 0) < totalPages); | ||
|
|
||
| if (!loadMore) { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (allFiles.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| await dispatch(downloadMultipleFiles(allFiles)); | ||
| }; | ||
| } | ||
|
|
||
| export function downloadMultipleFiles(files: FileResponse[]): AppThunk { | ||
| return async (dispatch, _getState) => { | ||
| // Prepare download options | ||
|
|
@@ -68,11 +117,13 @@ export function downloadMultipleFiles(files: FileResponse[]): AppThunk { | |
| options.push(MultipleDownloadOption.Backend); | ||
| } | ||
|
|
||
| const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This calculation of total size is not accurate: Folder size is always Suggesting removing this feature. |
||
|
|
||
| let finalOption = options[0]; | ||
| if (options.length > 1) { | ||
| try { | ||
| finalOption = (await dispatch( | ||
| selectOption(getDownloadSelectOption(options), "fileManager.selectArchiveMethod"), | ||
| selectOption(getDownloadSelectOption(options, totalSize), "fileManager.selectArchiveMethod"), | ||
| )) as MultipleDownloadOption; | ||
| } catch (e) { | ||
| // User cancel selection | ||
|
|
@@ -554,7 +605,10 @@ export function downloadSingleFile(file: FileResponse, preferredEntity?: string) | |
| }; | ||
| } | ||
|
|
||
| const getDownloadSelectOption = (options: MultipleDownloadOption[]): DialogSelectOption[] => { | ||
| const BROWSER_ARCHIVE_SIZE_LIMIT = 4 * 1024 * 1024 * 1024; // 4 GB | ||
|
|
||
| const getDownloadSelectOption = (options: MultipleDownloadOption[], totalSize: number): DialogSelectOption[] => { | ||
| const exceedsLimit = totalSize > BROWSER_ARCHIVE_SIZE_LIMIT; | ||
| return options.map((option): DialogSelectOption => { | ||
| switch (option) { | ||
| case MultipleDownloadOption.Backend: | ||
|
|
@@ -573,7 +627,13 @@ const getDownloadSelectOption = (options: MultipleDownloadOption[]): DialogSelec | |
| return { | ||
| value: MultipleDownloadOption.StreamSaver, | ||
| name: i18next.t("fileManager.browserBatchDownload"), | ||
| description: i18next.t("fileManager.browserBatchDownloadDescription"), | ||
| description: exceedsLimit | ||
| ? i18next.t("fileManager.browserBatchDownloadSizeExceeded", { | ||
| size: sizeToString(totalSize), | ||
| defaultValue: "Total size {{size}} exceeds the 4 GB browser archive limit", | ||
|
OnAirGame marked this conversation as resolved.
Outdated
|
||
| }) | ||
| : i18next.t("fileManager.browserBatchDownloadDescription"), | ||
| disabled: exceedsLimit, | ||
| }; | ||
| } | ||
| }); | ||
|
|
||



There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're using Claude Code, please update all other locales. It should be easy with the help of Claude.