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" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"git.elijahkuntz.com/Elijah/drive/config" "git.elijahkuntz.com/Elijah/drive/config"
"git.elijahkuntz.com/Elijah/drive/database" "git.elijahkuntz.com/Elijah/drive/database"
@ -41,11 +42,42 @@ type tusUpload struct {
} }
func NewTusHandler(db *database.DB, cfg *config.Config) *TusHandler { func NewTusHandler(db *database.DB, cfg *config.Config) *TusHandler {
return &TusHandler{ h := &TusHandler{
DB: db, DB: db,
Config: cfg, Config: cfg,
uploads: make(map[string]*tusUpload), 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. // Options returns tus protocol capability headers.

View file

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