feat(share): update outdated single file symbolic link metadata if renaming in source file is detected

This commit is contained in:
Aaron Liu 2025-04-21 19:54:45 +08:00
parent 721148b969
commit 922947de41
3 changed files with 69 additions and 78 deletions

View File

@ -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,
},
];

View File

@ -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<Promise<FileResponse>> {
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<Promise<DirectLink[]>> {
return async (dispatch, _getState): Promise<DirectLink[]> => {
const allFiles: FileResponse[] = [];

View File

@ -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) }));