diff --git a/src/redux/thunks/download.ts b/src/redux/thunks/download.ts index c437e3a..ec97699 100644 --- a/src/redux/thunks/download.ts +++ b/src/redux/thunks/download.ts @@ -17,13 +17,10 @@ import { } from "../../util/filesystem.ts"; import "../../util/zip.js"; import { closeContextMenu } from "../fileManagerSlice.ts"; -import { - DialogSelectOption, - setBatchDownloadLog, -} from "../globalStateSlice.ts"; +import { DialogSelectOption, setBatchDownloadLog } from "../globalStateSlice.ts"; import { AppThunk } from "../store.ts"; import { promiseId, selectOption } from "./dialog.ts"; -import { longRunningTaskWithSnackbar, walk, walkAll } from "./file.ts"; +import { longRunningTaskWithSnackbar, refreshSingleFileSymbolicLinks, walk, walkAll } from "./file.ts"; const streamSaverParam = "stream_saver"; @@ -54,17 +51,13 @@ export function downloadFiles(index: number, files: FileResponse[]): AppThunk { export function downloadMultipleFiles(files: FileResponse[]): AppThunk { return async (dispatch, _getState) => { // Prepare download options - const options: MultipleDownloadOption[] = [ - MultipleDownloadOption.StreamSaver, - ]; + const options: MultipleDownloadOption[] = [MultipleDownloadOption.StreamSaver]; // @ts-ignore if (window.isSecureContext && window.showDirectoryPicker) { options.push(MultipleDownloadOption.Browser); } - const groupPermission = new Boolset( - SessionManager.currentUser()?.group?.permission, - ); + const groupPermission = new Boolset(SessionManager.currentUser()?.group?.permission); if ( groupPermission.enabled(GroupPermission.archive_download) && (files.length > 1 || !files[0].metadata?.[Metadata.share_redirect]) @@ -76,10 +69,7 @@ export function downloadMultipleFiles(files: FileResponse[]): AppThunk { if (options.length > 1) { try { finalOption = (await dispatch( - selectOption( - getDownloadSelectOption(options), - "fileManager.selectArchiveMethod", - ), + selectOption(getDownloadSelectOption(options), "fileManager.selectArchiveMethod"), )) as MultipleDownloadOption; } catch (e) { // User cancel selection @@ -136,14 +126,10 @@ export function browserBatchDownload(files: FileResponse[]): AppThunk { // we should obtain the readwrite permission for the directory at first if (!(await verifyFileSystemRWPermission(handle))) { enqueueSnackbar({ - message: i18next.t( - "application:fileManager.directoryDownloadPermissionError", - ), + message: i18next.t("application:fileManager.directoryDownloadPermissionError"), variant: "error", }); - throw new Error( - i18next.t("application:fileManager.directoryDownloadPermissionError"), - ); + throw new Error(i18next.t("application:fileManager.directoryDownloadPermissionError")); } } catch (e) { return; @@ -199,9 +185,7 @@ function startBrowserBatchDownloadTo( failed++; continue; } - const name = - (relativePath == "" ? "" : relativePath + "/") + - childFiles[i].name; + const name = (relativePath == "" ? "" : relativePath + "/") + childFiles[i].name; if (fsPaths.has(name)) { if (skipAll) { appendLog( @@ -223,10 +207,7 @@ function startBrowserBatchDownloadTo( let overwriteOption = DownloadOverwriteOption.Skip; try { overwriteOption = (await dispatch( - selectOption( - getDownloadOverwriteOption(name), - "fileManager.selectDirectoryDuplicationMethod", - ), + selectOption(getDownloadOverwriteOption(name), "fileManager.selectDirectoryDuplicationMethod"), )) as DownloadOverwriteOption; } catch (e) { // User cancel, use skip option @@ -248,9 +229,7 @@ function startBrowserBatchDownloadTo( ); skipAll = true; continue; - } else if ( - overwriteOption == DownloadOverwriteOption.OverwriteAll - ) { + } else if (overwriteOption == DownloadOverwriteOption.OverwriteAll) { appendLog( i18next.t("modals.directoryDownloadReplaceNotifiction", { name, @@ -272,14 +251,8 @@ function startBrowserBatchDownloadTo( const res = await fetch(entityUrls.urls[i], { signal: cancelSignals[downloadId].signal, }); - await saveFileToFileSystemDirectory( - handle, - await res.blob(), - name, - ); - appendLog( - i18next.t("modals.directoryDownloadFinished", { name }), - ); + await saveFileToFileSystemDirectory(handle, await res.blob(), name); + appendLog(i18next.t("modals.directoryDownloadFinished", { name })); } catch (e) { // User cancel download if (e instanceof Error && e.name == "AbortError") { @@ -312,9 +285,7 @@ function startBrowserBatchDownloadTo( if (failed === 0) { appendLog(i18next.t("fileManager.directoryDownloadFinished")); } else { - appendLog( - i18next.t("fileManager.directoryDownloadFinishedWithError", { failed }), - ); + appendLog(i18next.t("fileManager.directoryDownloadFinishedWithError", { failed })); } }; } @@ -322,15 +293,10 @@ function startBrowserBatchDownloadTo( export function streamSaverDownload(files: FileResponse[]): AppThunk { return async (dispatch, getState) => { const allFiles = ( - await longRunningTaskWithSnackbar( - dispatch(walkAll(files)), - "application:fileManager.preparingBathDownload", - ) + await longRunningTaskWithSnackbar(dispatch(walkAll(files)), "application:fileManager.preparingBathDownload") ).filter((f) => f.type == FileType.file); - const fileStream = streamSaver.createWriteStream( - formatLocalTime(dayjs()) + ".zip", - ); + const fileStream = streamSaver.createWriteStream(formatLocalTime(dayjs()) + ".zip"); const { siteConfig: { explorer: { @@ -379,10 +345,7 @@ export function streamSaverDownload(files: FileResponse[]): AppThunk { if (window.WritableStream && readableZipStream.pipeTo) { try { - await longRunningTaskWithSnackbar( - readableZipStream.pipeTo(fileStream), - "fileManager.batchDownloadStarted", - ); + await longRunningTaskWithSnackbar(readableZipStream.pipeTo(fileStream), "fileManager.batchDownloadStarted"); } catch (e) { console.log(e); } @@ -390,11 +353,13 @@ export function streamSaverDownload(files: FileResponse[]): AppThunk { }; } -export function downloadSingleFile( - file: FileResponse, - preferredEntity?: string, -): AppThunk { +export function downloadSingleFile(file: FileResponse, preferredEntity?: string): AppThunk { return async (dispatch, _getState) => { + const isSharedFile = file.metadata?.[Metadata.share_redirect] ?? false; + if (isSharedFile) { + file = await dispatch(refreshSingleFileSymbolicLinks(file)); + } + const urlRes = await longRunningTaskWithSnackbar( dispatch( getFileEntityUrl({ @@ -427,9 +392,7 @@ export function downloadSingleFile( variant: "loading", persist: true, }); - return readableStream - .pipeTo(fileStream) - .finally(() => closeSnackbar(downloadingSnackbar)); + return readableStream.pipeTo(fileStream).finally(() => closeSnackbar(downloadingSnackbar)); } } else { window.location.assign(urlRes.urls[0]); @@ -437,9 +400,7 @@ export function downloadSingleFile( }; } -const getDownloadSelectOption = ( - options: MultipleDownloadOption[], -): DialogSelectOption[] => { +const getDownloadSelectOption = (options: MultipleDownloadOption[]): DialogSelectOption[] => { return options.map((option): DialogSelectOption => { switch (option) { case MultipleDownloadOption.Backend: @@ -468,10 +429,7 @@ const getDownloadOverwriteOption = (name: string): DialogSelectOption[] => { return [ { name: i18next.t("fileManager.directoryDownloadReplace"), - description: i18next.t( - "fileManager.directoryDownloadReplaceDescription", - { name }, - ), + description: i18next.t("fileManager.directoryDownloadReplaceDescription", { name }), value: DownloadOverwriteOption.Overwrite, }, { @@ -483,18 +441,12 @@ const getDownloadOverwriteOption = (name: string): DialogSelectOption[] => { }, { name: i18next.t("fileManager.directoryDownloadReplaceAll"), - description: i18next.t( - "fileManager.directoryDownloadReplaceAllDescription", - { name }, - ), + description: i18next.t("fileManager.directoryDownloadReplaceAllDescription", { name }), value: DownloadOverwriteOption.OverwriteAll, }, { name: i18next.t("fileManager.directoryDownloadSkipAll"), - description: i18next.t( - "fileManager.directoryDownloadSkipAllDescription", - { name }, - ), + description: i18next.t("fileManager.directoryDownloadSkipAllDescription", { name }), value: DownloadOverwriteOption.SkipAll, }, ]; diff --git a/src/redux/thunks/file.ts b/src/redux/thunks/file.ts index 800175d..d9e9ab2 100644 --- a/src/redux/thunks/file.ts +++ b/src/redux/thunks/file.ts @@ -905,8 +905,10 @@ export function goToSharedLink(index: number, file: FileResponse): AppThunk { const shareUri = file.metadata?.[Metadata.share_redirect]; if (shareUri) { // Add a delay for animation to close the context menu + const shareUriParsed = new CrUri(shareUri); + shareUriParsed.setPath(""); setTimeout(() => { - dispatch(navigateToPath(index, shareUri)); + dispatch(navigateToPath(index, shareUriParsed.toString())); }, contextMenuCloseAnimationDelay); } }; @@ -1140,6 +1142,38 @@ export function batchGetDirectLinks(index: number, files: FileResponse[]): AppTh }; } +// Single file symbolic links might be invalid if original file is renamed by its owner, +// we need to refresh the symbolic links by getting the latest file list +export function refreshSingleFileSymbolicLinks(file: FileResponse): AppThunk> { + return async (dispatch, _getState) => { + if (file.type != FileType.file || !file?.metadata?.[Metadata.share_redirect]) { + return file; + } + const currentUrl = new CrUri(getFileLinkedUri(file)); + const latestList = await dispatch( + getFileList({ + uri: currentUrl.setPath("").toString(), + page_size: 50, + }), + ); + if (latestList.files.length != 1) { + return file; + } + const latestFile = latestList.files[0]; + if (!latestFile) { + return file; + } + + if (latestFile.path != file?.metadata?.[Metadata.share_redirect]) { + // File renamed, update file share_redirect + dispatch( + patchFileMetadata(FileManagerIndex.main, [file], [{ key: Metadata.share_redirect, value: latestFile.path }]), + ); + } + return latestFile; + }; +} + function startBatchGetDirectLinks(files: FileResponse[]): AppThunk> { return async (dispatch, _getState): Promise => { const allFiles: FileResponse[] = []; diff --git a/src/redux/thunks/viewer.ts b/src/redux/thunks/viewer.ts index bd8aed2..c5e9e65 100644 --- a/src/redux/thunks/viewer.ts +++ b/src/redux/thunks/viewer.ts @@ -32,7 +32,7 @@ import { import { Viewers, ViewersByID } from "../siteConfigSlice.ts"; import { AppThunk } from "../store.ts"; import { askSaveAs, askStaleVersionAction } from "./dialog.ts"; -import { longRunningTaskWithSnackbar } from "./file.ts"; +import { longRunningTaskWithSnackbar, refreshSingleFileSymbolicLinks } from "./file.ts"; export interface ExpandedViewerSetting { [key: string]: Viewer[]; @@ -112,8 +112,13 @@ export function openViewer(file: FileResponse, viewer: Viewer, size: number, pre action: DefaultCloseAction, }); } + + const isSharedFile = file.metadata?.[Metadata.share_redirect] ?? false; + if (isSharedFile) { + file = await dispatch(refreshSingleFileSymbolicLinks(file)); + } + if (viewer.type == ViewerType.builtin) { - const isSharedFile = file.metadata?.[Metadata.share_redirect] ?? false; let primaryEntity = file.primary_entity; if (isSharedFile) { const fileInfo = await dispatch(getFileInfo({ uri: getFileLinkedUri(file) }));