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
Some checks failed
Automated Container Build / build-and-push (push) Failing after 15s
This commit is contained in:
parent
b668418951
commit
3e34a37c95
2 changed files with 73 additions and 38 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Reference in a new issue