fix file manager navigation after permission errors (#1103)

This commit is contained in:
ZacharyZcR
2026-07-28 01:48:07 +08:00
committed by GitHub
parent 6866176778
commit 697e363b74
2 changed files with 32 additions and 10 deletions
+23 -3
View File
@@ -106,6 +106,9 @@ function FileManagerContent({
const [currentPath, setCurrentPath] = useState( const [currentPath, setCurrentPath] = useState(
initialPath || initialHost?.defaultPath || "/", initialPath || initialHost?.defaultPath || "/",
); );
const lastSuccessfulPathRef = useRef(
initialPath || initialHost?.defaultPath || "/",
);
const [navHistory, setNavHistory] = useState<string[]>([ const [navHistory, setNavHistory] = useState<string[]>([
initialPath || initialHost?.defaultPath || "/", initialPath || initialHost?.defaultPath || "/",
]); ]);
@@ -497,6 +500,7 @@ function FileManagerContent({
? response ? response
: response?.files || []; : response?.files || [];
setFiles(files); setFiles(files);
lastSuccessfulPathRef.current = currentPath;
clearSelection(); clearSelection();
initialLoadDoneRef.current = true; initialLoadDoneRef.current = true;
@@ -565,7 +569,7 @@ function FileManagerContent({
} }
const loadDirectory = useCallback( const loadDirectory = useCallback(
async (path: string): Promise<boolean> => { async (path: string, conflictAttempt = 0): Promise<boolean> => {
if (!sshSessionId) { if (!sshSessionId) {
console.error("Cannot load directory: no SSH session ID"); console.error("Cannot load directory: no SSH session ID");
return false; return false;
@@ -595,6 +599,7 @@ function FileManagerContent({
: response?.files || []; : response?.files || [];
setFiles(files); setFiles(files);
lastSuccessfulPathRef.current = resolvedPath;
clearSelection(); clearSelection();
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
@@ -617,12 +622,27 @@ function FileManagerContent({
const httpStatus = apiError.status ?? apiError.response?.status; const httpStatus = apiError.status ?? apiError.response?.status;
// 409 = concurrent request already in flight — silently drop // The sidebar may be listing the same path to populate its tree.
// Retry instead of leaving the breadcrumb and visible files out of sync.
if (httpStatus === 409) { if (httpStatus === 409) {
if (conflictAttempt < 3) {
await new Promise((resolve) => setTimeout(resolve, 250));
return loadDirectory(resolvedPath, conflictAttempt + 1);
}
const previousPath = lastSuccessfulPathRef.current;
lastPathChangeRef.current = previousPath;
setCurrentPath((current) =>
current === resolvedPath ? previousPath : current,
);
return false; return false;
} }
if (apiError.response?.data?.needsSudo) { if (apiError.response?.data?.needsSudo) {
const previousPath = lastSuccessfulPathRef.current;
lastPathChangeRef.current = previousPath;
setCurrentPath((current) =>
current === resolvedPath ? previousPath : current,
);
if (!sudoDialogOpen) { if (!sudoDialogOpen) {
setPendingSudoOperation({ type: "navigate", path: resolvedPath }); setPendingSudoOperation({ type: "navigate", path: resolvedPath });
setSudoDialogOpen(true); setSudoDialogOpen(true);
@@ -700,7 +720,7 @@ function FileManagerContent({
} }
} }
}, },
[sshSessionId, isLoading, clearSelection, t, sudoDialogOpen, currentHost], [sshSessionId, clearSelection, t, sudoDialogOpen, currentHost],
); );
const debouncedLoadDirectory = useCallback( const debouncedLoadDirectory = useCallback(
@@ -228,8 +228,8 @@ export function FileManagerSidebar({
* Called the first time a folder is expanded via FolderTree's onSelect. * Called the first time a folder is expanded via FolderTree's onSelect.
*/ */
const loadSubdirectory = useCallback( const loadSubdirectory = useCallback(
async (folderId: string, folderPath: string) => { async (folderId: string, folderPath: string): Promise<boolean> => {
if (!sshSessionId) return; if (!sshSessionId) return false;
try { try {
const subResponse = await listSSHFiles(sshSessionId, folderPath); const subResponse = await listSSHFiles(sshSessionId, folderPath);
@@ -262,16 +262,20 @@ export function FileManagerSidebar({
}); });
return updateChildren(prevTree); return updateChildren(prevTree);
}); });
loadedFoldersRef.current.add(folderPath);
return true;
} catch (error: unknown) { } catch (error: unknown) {
const status = const status =
(error as { status?: number })?.status || (error as { status?: number })?.status ||
(error as { response?: { status?: number } })?.response?.status; (error as { response?: { status?: number } })?.response?.status;
if (status === 409) { if (status === 409) {
// Another request was listing this path — retry after the lock clears // Another request was listing this path — retry after the lock clears
setTimeout(() => loadSubdirectory(folderId, folderPath), 600); setTimeout(() => void loadSubdirectory(folderId, folderPath), 600);
return; return false;
} }
loadedFoldersRef.current.delete(folderPath);
console.error("Failed to load subdirectory:", error); console.error("Failed to load subdirectory:", error);
return false;
} }
}, },
[sshSessionId], [sshSessionId],
@@ -309,8 +313,7 @@ export function FileManagerSidebar({
const parent = findByPath(directoryTree); const parent = findByPath(directoryTree);
if (parent && !loadedFoldersRef.current.has(parent.path)) { if (parent && !loadedFoldersRef.current.has(parent.path)) {
loadedFoldersRef.current.add(parent.path); void loadSubdirectory(parent.id, parent.path);
loadSubdirectory(parent.id, parent.path);
} }
}, [currentPath, directoryTree, loadSubdirectory, sshSessionId]); }, [currentPath, directoryTree, loadSubdirectory, sshSessionId]);
@@ -416,7 +419,6 @@ export function FileManagerSidebar({
item.path !== "/" && item.path !== "/" &&
!loadedFoldersRef.current.has(item.path) !loadedFoldersRef.current.has(item.path)
) { ) {
loadedFoldersRef.current.add(item.path);
await loadSubdirectory(id, item.path); await loadSubdirectory(id, item.path);
} }
}, },