feat: re-architect folder uploads with batching and concurrency limits, add TUS stale cleanup routine
Some checks failed
Automated Container Build / build-and-push (push) Failing after 15s

This commit is contained in:
Elijah 2026-05-23 13:41:49 -07:00
parent b668418951
commit 3e34a37c95
2 changed files with 73 additions and 38 deletions

View file

@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"sync"
"time"
"git.elijahkuntz.com/Elijah/drive/config"
"git.elijahkuntz.com/Elijah/drive/database"
@ -41,11 +42,42 @@ type tusUpload struct {
}
func NewTusHandler(db *database.DB, cfg *config.Config) *TusHandler {
return &TusHandler{
h := &TusHandler{
DB: db,
Config: cfg,
uploads: make(map[string]*tusUpload),
}
go h.cleanupStaleUploads()
return h
}
func (h *TusHandler) cleanupStaleUploads() {
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
tempDir := filepath.Join(h.Config.StorageDir, ".uploads")
entries, err := os.ReadDir(tempDir)
if err != nil {
continue
}
cutoff := time.Now().Add(-24 * time.Hour)
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
path := filepath.Join(tempDir, entry.Name())
os.Remove(path)
h.mu.Lock()
delete(h.uploads, entry.Name())
h.mu.Unlock()
}
}
}
}
// Options returns tus protocol capability headers.

View file

@ -1855,45 +1855,48 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</button>
</div>
<div className="max-h-60 overflow-y-auto p-2 space-y-1" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
{uploads.map(upload => (
<div key={upload.id} className="p-3 rounded-xl" style={{ backgroundColor: 'var(--color-bg-elevated)' }}>
<div className="flex items-center justify-between mb-1">
<span
className="text-xs font-medium truncate max-w-[200px]"
style={{ color: 'var(--color-text-primary)' }}
title={getFileDisplayName(upload.name, false)}
{uploads.map(batch => {
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">
<span
className="text-xs font-medium truncate max-w-[200px]"
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>
{batch.totalFiles > 1 && (
<div className="text-[10px] mb-2" style={{ color: 'var(--color-text-secondary)' }}>
{batch.filesUploaded} / {batch.totalFiles} files
</div>
)}
<div
className="h-1.5 w-full rounded-full overflow-hidden"
style={{ backgroundColor: 'var(--color-bg-secondary)' }}
>
{getFileDisplayName(upload.name, false)}
</span>
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
{upload.status === 'complete' ? '✓' : upload.status === 'error' ? '✗' : `${Math.round(upload.progress)}%`}
</span>
<motion.div
className="h-full rounded-full"
initial={{ width: 0 }}
animate={{ width: `${percentage}%` }}
style={{
backgroundColor: batch.status === 'error' ? 'var(--color-danger)' : 'var(--color-accent)',
}}
/>
</div>
{batch.error && (
<div className="text-[10px] mt-2 line-clamp-3" style={{ color: 'var(--color-danger)' }}>
{batch.error}
</div>
)}
</div>
<div
className="h-1.5 w-full rounded-full overflow-hidden"
style={{ backgroundColor: 'var(--color-bg-secondary)' }}
>
<motion.div
className="h-full rounded-full"
initial={{ width: 0 }}
animate={{ width: `${upload.progress}%` }}
style={{
backgroundColor:
upload.status === 'error'
? 'var(--color-danger)'
: upload.status === 'complete'
? 'var(--color-success)'
: 'var(--color-accent)',
}}
/>
</div>
{upload.status === 'error' && (
<p className="text-xs mt-1" style={{ color: 'var(--color-danger)' }}>
{upload.error}
</p>
)}
</div>
))}
);
})}
</div>
</motion.div>
)}