feat(api, ui): handle TUS lockups, automatic chunk token refresh, and add upload cancellation
Some checks failed
Automated Container Build / build-and-push (push) Failing after 9s

This commit is contained in:
Elijah 2026-05-23 17:53:56 -07:00
parent 36d97ac330
commit 91fc088389
2 changed files with 71 additions and 17 deletions

View file

@ -105,8 +105,9 @@ interface UploadBatch {
uploadedBytes: number;
totalFiles: number;
filesUploaded: number;
status: 'uploading' | 'complete' | 'error';
status: 'uploading' | 'complete' | 'error' | 'canceled';
error?: string;
abortController?: AbortController;
}
interface Toast {
@ -908,8 +909,11 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
uploadedBytes: 0,
totalFiles,
filesUploaded: 0,
status: 'uploading'
}]);
status: 'uploading' as const,
abortController: new AbortController()
};
setUploads(prev => [...prev, newBatch]);
const MAX_CONCURRENT = 4;
let currentIndex = 0;
@ -934,8 +938,10 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const uploadPath = path ? (destPath === '.' ? path : `${destPath}/${path}`) : destPath;
try {
if (newBatch.abortController.signal.aborted) throw new Error('Upload canceled');
await api.upload(file, uploadPath, {
overwrite,
signal: newBatch.abortController.signal,
onProgress: (percent) => {
fileProgressMap.set(file.name + path, (percent / 100) * file.size);
updateProgress();
@ -945,7 +951,12 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
fileProgressMap.set(file.name + path, file.size);
updateProgress();
} catch (err: any) {
setUploads(prev => prev.map(b => b.id === batchId ? { ...b, status: 'error', error: err.message || 'Upload failed' } : b));
setUploads(prev => prev.map(b => b.id === batchId ? {
...b,
status: err.message === 'Upload canceled' ? 'canceled' : 'error',
error: err.message || 'Upload failed'
} : b));
if (err.message === 'Upload canceled') return;
}
}
};
@ -2093,7 +2104,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</span>
</div>
<button
onClick={() => setUploads(prev => prev.filter(u => u.status === 'uploading'))}
onClick={() => setUploads(prev => prev.filter(u => u.status !== 'uploading' && u.status !== 'error' && u.status !== 'canceled'))}
className="p-1 rounded hover:bg-white/10"
>
<X className="w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
@ -2104,17 +2115,28 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const percentage = batch.totalBytes > 0 ? (batch.uploadedBytes / batch.totalBytes) * 100 : 0;
return (
<div key={batch.id} className="p-3 rounded-xl" style={{ backgroundColor: 'var(--color-bg-elevated)' }}>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center justify-between mb-2">
<span
className="text-xs font-medium truncate max-w-[200px]"
className="text-xs font-medium truncate max-w-[180px]"
style={{ color: 'var(--color-text-primary)' }}
title={batch.name}
>
{batch.name}
</span>
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
{batch.status === 'complete' ? '✓' : batch.status === 'error' ? '✗' : `${Math.round(percentage)}%`}
</span>
<div className="flex items-center gap-3">
<span className="text-xs font-semibold" style={{ color: 'var(--color-text-secondary)' }}>
{batch.status === 'canceled' ? 'Canceled' : `${Math.round(percentage)}%`}
</span>
{batch.status === 'uploading' && (
<button
onClick={() => batch.abortController?.abort()}
className="p-1 rounded-full hover:bg-white/10 transition-colors"
title="Cancel upload"
>
<X className="w-3 h-3 text-red-400 hover:text-red-300" />
</button>
)}
</div>
</div>
{batch.totalFiles > 1 && (
<div className="text-[10px] mb-2" style={{ color: 'var(--color-text-secondary)' }}>