mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-07 09:53:17 +00:00
fix(torrents): harden removal reservation recovery
This commit is contained in:
@@ -57,6 +57,8 @@ type CommandMap = {
|
||||
remove_download: { args: { id: string; deleteAssets: boolean; preserveResumable?: boolean }; result: void };
|
||||
get_download_primary_path: { args: { id: string }; result: string | null };
|
||||
detach_download_for_reconfigure: { args: { id: string }; result: void };
|
||||
clear_torrent_removal_paths: { args: { id: string }; result: void };
|
||||
reconcile_torrent_removal_reservations: { args: undefined; result: number };
|
||||
begin_dock_badge_session: { args: undefined; result: number };
|
||||
update_dock_badge: { args: { count: number; generation: number; session: number }; result: void };
|
||||
get_platform_info: { args: undefined; result: PlatformInfo };
|
||||
|
||||
@@ -135,6 +135,38 @@ describe('useDownloadStore', () => {
|
||||
expect(fileName.endsWith('.mp4')).toBe(true);
|
||||
});
|
||||
|
||||
it('clears a persisted Torrent removal reservation when a paused item disables cleanup', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [{
|
||||
id: 'paused-torrent-removal',
|
||||
url: 'magnet:?xt=urn:btih:abc',
|
||||
fileName: 'torrent',
|
||||
status: 'paused',
|
||||
category: 'Other',
|
||||
dateAdded: '',
|
||||
isTorrent: true,
|
||||
torrentFileIndices: [0],
|
||||
torrentRemoveUnselectedFile: true
|
||||
}] as any[],
|
||||
backendRegisteredIds: new Set(['paused-torrent-removal'])
|
||||
});
|
||||
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
|
||||
|
||||
await useDownloadStore.getState().applyProperties('paused-torrent-removal', {
|
||||
torrentRemoveUnselectedFile: false
|
||||
});
|
||||
|
||||
expect(ipc.invokeCommand).toHaveBeenCalledWith(
|
||||
'detach_download_for_reconfigure',
|
||||
{ id: 'paused-torrent-removal' }
|
||||
);
|
||||
expect(ipc.invokeCommand).toHaveBeenCalledWith(
|
||||
'clear_torrent_removal_paths',
|
||||
{ id: 'paused-torrent-removal' }
|
||||
);
|
||||
expect(useDownloadStore.getState().downloads[0].torrentRemoveUnselectedFile).toBe(false);
|
||||
});
|
||||
|
||||
it('replaces stale media intent when an appended handoff reuses a URL', () => {
|
||||
useDownloadStore.getState().openAddModalWithUrls(
|
||||
'https://example.com/file.bin', '', '', '', '', true
|
||||
|
||||
@@ -913,12 +913,18 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
const normalizedUpdates = updates.fileName === undefined
|
||||
? updates
|
||||
: { ...updates, fileName: canonicalizeDownloadFileName(updates.fileName) };
|
||||
const disablingTorrentRemoval = item.isTorrent === true
|
||||
&& normalizedUpdates.torrentRemoveUnselectedFile === false
|
||||
&& item.torrentRemoveUnselectedFile !== false;
|
||||
|
||||
if (item.status === 'downloading' || item.status === 'processing' || item.status === 'seeding' || item.status === 'retrying') {
|
||||
throw new Error(i18n.t($ => $.downloadTable.transferActive));
|
||||
}
|
||||
|
||||
if (item.status === 'ready' || item.status === 'staged' || item.status === 'completed' || item.status === 'failed') {
|
||||
if (disablingTorrentRemoval) {
|
||||
await invoke('clear_torrent_removal_paths', { id });
|
||||
}
|
||||
state.updateDownload(id, normalizedUpdates);
|
||||
return;
|
||||
}
|
||||
@@ -932,6 +938,9 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
state.unregisterBackendIds([id]);
|
||||
set(current => ({ pendingOrder: current.pendingOrder.filter(value => value !== id) }));
|
||||
}
|
||||
if (disablingTorrentRemoval) {
|
||||
await invoke('clear_torrent_removal_paths', { id });
|
||||
}
|
||||
state.updateDownload(id, normalizedUpdates);
|
||||
if (isRegistered || wasDispatching) {
|
||||
const dispatched = await dispatchItemInternal(id);
|
||||
@@ -951,6 +960,9 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
}
|
||||
state.unregisterBackendIds([id]);
|
||||
}
|
||||
if (disablingTorrentRemoval) {
|
||||
await invoke('clear_torrent_removal_paths', { id });
|
||||
}
|
||||
state.updateDownload(id, normalizedUpdates);
|
||||
}
|
||||
};
|
||||
@@ -2386,6 +2398,16 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
: state.downloads
|
||||
}));
|
||||
|
||||
// A process can die after Aria2 has removed the unselected files but
|
||||
// before the terminal event clears Firelink's reservation. Reclaim
|
||||
// only the conservative terminal cases in the backend before queued
|
||||
// downloads are allowed to claim paths on startup.
|
||||
try {
|
||||
await invoke('reconcile_torrent_removal_reservations');
|
||||
} catch (error) {
|
||||
console.warn('Could not reconcile Torrent removal reservations during startup:', error);
|
||||
}
|
||||
|
||||
// The backend dispatcher is live before the frontend finishes startup.
|
||||
// Synchronize the normalized queue policy before any saved download is
|
||||
// allowed to claim a permit.
|
||||
|
||||
Reference in New Issue
Block a user