Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3815,7 +3815,10 @@ <h3 id="portableTitle"><span data-icon="disc"></span>Portable session</h3>
*/
function beginScanChrome({ quiet = false } = {}) {
state.scanning = true;
$('scanBtn').disabled = true;
$('scanBtn').disabled = false;
$('scanBtn').classList.remove('btn-primary');
$('scanBtn').classList.add('btn-danger');
$('scanBtn').innerHTML = icon('x', 16) + 'Stop';
$('headerSpin').classList.add('on');
$('headerSpin').innerHTML = icon('loader', 15, REDUCED ? '' : 'spin');
$('progressTrack').classList.add('active');
Expand Down Expand Up @@ -4096,6 +4099,9 @@ <h3 id="portableTitle"><span data-icon="disc"></span>Portable session</h3>
function endScanChrome() {
state.scanning = false;
$('scanBtn').disabled = false;
$('scanBtn').classList.remove('btn-danger');
$('scanBtn').classList.add('btn-primary');
$('scanBtn').innerHTML = icon('play', 16) + 'Scan';
$('headerSpin').classList.remove('on');
$('progressTrack').classList.remove('active');
$('scanMeta').textContent = '';
Expand Down Expand Up @@ -4332,6 +4338,14 @@ <h3 id="portableTitle"><span data-icon="disc"></span>Portable session</h3>
}

$('scanBtn').addEventListener('click', () => {
if (state.scanning) {
if (state.scanId) {
api(`/api/scan/${state.scanId}/cancel`, { method: 'POST' }).catch(() => {});
closeEventSource();
failScan('Scan stopped by user');
}
return;
}
const p = $('pathInput').value.trim();
if (!p) { toast('Enter a folder path first', 'error'); return; }
startScan(p, { incremental: !$('fastRescanWrap').hidden && $('fastRescan').checked });
Expand Down
12 changes: 12 additions & 0 deletions src/api/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,18 @@ export const ENDPOINTS: EndpointDescriptor[] = [
'404': errorResponse('Path does not exist'),
},
},
{
method: 'post',
path: '/api/scan/{scanId}/cancel',
summary: 'Cancel a running scan',
tag: 'scan',
destructive: false,
parameters: [pathParam('scanId', 'Scan id')],
responses: {
'200': jsonResponse('Scan cancellation requested', obj({ scanId: str(), cancelled: bool() }, ['scanId', 'cancelled'])),
'404': errorResponse('Unknown scanId'),
},
},
{
method: 'get',
path: '/api/scan/{scanId}/progress',
Expand Down
8 changes: 7 additions & 1 deletion src/api/scanRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router, Request, Response } from 'express';
import { startScan, getScan, collectLargestFiles, collectFileTypes } from '../services/diskScanner';
import { startScan, getScan, cancelScan, collectLargestFiles, collectFileTypes } from '../services/diskScanner';
import { buildTreemapFromStore } from '../utils/treemap';
import { pruneTree, PruneResult } from '../utils/pruneTree';
import { isInside } from '../utils/pathSanitizer';
Expand Down Expand Up @@ -163,6 +163,12 @@ scanRouter.post('/scan', guardBodyPath, async (req: Request, res: Response) => {
res.status(202).json({ scanId: scan.scanId, incremental: scan.incremental === true });
});

/** POST /api/scan/:scanId/cancel — stop a running scan */
scanRouter.post('/scan/:scanId/cancel', (req: Request, res: Response) => {
const ok = cancelScan(String(req.params.scanId));
res.json({ scanId: req.params.scanId, cancelled: ok });
});

/** GET /api/scan/:scanId/progress — Server-Sent Events stream. */
scanRouter.get('/scan/:scanId/progress', (req: Request, res: Response) => {
const scan = requireScan(req, req.params.scanId);
Expand Down
19 changes: 18 additions & 1 deletion src/services/diskScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ export function cancelAllScans(): void {
for (const scan of scans.values()) scan.cancelled = true;
}

export function cancelScan(scanId: string): boolean {
const scan = scans.get(scanId);
if (scan && scan.status === 'running') {
scan.cancelled = true;
scan.status = 'error';
scan.error = 'Scan cancelled by user';
scan.finishedAt = Date.now();
return true;
}
return false;
}

/**
* Compatibility accessor: every production scan's tree lives in `scan.store`,
* and no production code path reads or writes `scan.root` (the bounded
Expand Down Expand Up @@ -425,7 +437,12 @@ async function walk(scan: ScanResult, rootIsDir: boolean, ignore: CompiledIgnore
// before their cached listing is trusted — membership is per-scan.
await drainQueue(scan, store, [{ id: store.rootId, path: scan.rootPath, cached: null, revalidate: false }], ignore, cache, new Set<string>());
}
if (scan.cancelled) return;
if (scan.cancelled) {
scan.status = 'error';
scan.error = scan.error ?? 'Scan cancelled by user';
scan.finishedAt = Date.now();
return;
}

store.finalize();
store.sumSizes();
Expand Down