From 5b3cb9e393f8df1eb2d87304ae2ab65d677eedb7 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Thu, 23 Apr 2026 22:00:14 +0300 Subject: [PATCH] =?UTF-8?q?copier:=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D1=81=20=D0=BF=D0=BE=20=D0=B1=D0=B0=D0=B9=D1=82=D0=B0?= =?UTF-8?q?=D0=BC,=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B8=20ETA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Прогрессбар по скопированным байтам (doneBytes / totalBytes) - SpeedBPS и ETASec добавлены в Task - Dashboard показывает скорость (МБ/с) и ETA справа от прогрессбара Co-Authored-By: Claude Sonnet 4.6 --- internal/copier/copier.go | 39 +++++++++++++++++++++++++++++++----- internal/task/task.go | 2 ++ web/templates/dashboard.html | 26 +++++++++++++++++++++++- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/internal/copier/copier.go b/internal/copier/copier.go index b5d69a0..feed7f5 100644 --- a/internal/copier/copier.go +++ b/internal/copier/copier.go @@ -9,6 +9,7 @@ import ( "os/exec" "path/filepath" "sync" + "time" "jukebox_maker/internal/config" "jukebox_maker/internal/db" @@ -150,36 +151,63 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database return } + // суммарный объём для прогресса (всех файлов в списке) + var totalBytes int64 + for _, f := range files { + totalBytes += f.size + } + total := len(files) copied := 0 + var doneBytes int64 + startTime := time.Now() + for i, f := range files { select { case <-ctx.Done(): c.tasks.Update(taskID, func(t *task.Task) { t.Status = task.StatusCanceled t.Message = "Отменено" + t.SpeedBPS = 0 + t.ETASec = 0 }) return default: } if f.size > available { - // файл не влезает — пробуем следующий continue } - prog := int(float64(i+1) / float64(total) * 100) + elapsed := time.Since(startTime).Seconds() + var speedBPS, etaSec int64 + if elapsed > 0 && doneBytes > 0 { + speedBPS = int64(float64(doneBytes) / elapsed) + remaining := totalBytes - doneBytes + if speedBPS > 0 { + etaSec = remaining / speedBPS + } + } + + prog := int(float64(doneBytes) / float64(totalBytes) * 100) msg := fmt.Sprintf("Копирование %s (%d/%d)", filepath.Base(f.srcAbs), i+1, total) - setStatus(task.StatusRunning, msg, prog) - // destination mirrors source structure under destRoot + c.tasks.Update(taskID, func(t *task.Task) { + t.Status = task.StatusRunning + t.Message = msg + t.Progress = prog + t.SpeedBPS = speedBPS + t.ETASec = int(etaSec) + }) + dstAbs := filepath.Join(destRoot, f.relPath) - if err := rsyncFile(ctx, f.srcAbs, dstAbs); err != nil { if errors.Is(err, context.Canceled) { c.tasks.Update(taskID, func(t *task.Task) { t.Status = task.StatusCanceled t.Message = "Отменено" + t.SpeedBPS = 0 + t.ETASec = 0 }) return } @@ -187,6 +215,7 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database } available -= f.size + doneBytes += f.size copied++ _ = database.RecordCopy(db.CopyRecord{ DiskID: opts.DiskID, diff --git a/internal/task/task.go b/internal/task/task.go index 1eb397e..cb2b8a3 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -23,6 +23,8 @@ type Task struct { Status Status `json:"status"` Progress int `json:"progress"` Message string `json:"message"` + SpeedBPS int64 `json:"speed_bps"` + ETASec int `json:"eta_sec"` Error string `json:"error"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html index 12f1205..574e6ed 100644 --- a/web/templates/dashboard.html +++ b/web/templates/dashboard.html @@ -30,7 +30,10 @@
-
Подготовка…
+
+
Подготовка…
+
+
@@ -83,6 +86,21 @@ async function refreshDisk() { function startTaskPoll(id) { stopTaskPoll(); pollInterval = setInterval(() => pollTask(id), 1500); } function stopTaskPoll() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } } +function fmtSpeed(bps) { + if (!bps) return ''; + if (bps >= 1e9) return (bps/1e9).toFixed(1) + ' ГБ/с'; + if (bps >= 1e6) return (bps/1e6).toFixed(1) + ' МБ/с'; + if (bps >= 1e3) return (bps/1e3).toFixed(0) + ' КБ/с'; + return bps + ' Б/с'; +} + +function fmtETA(sec) { + if (!sec || sec <= 0) return ''; + if (sec >= 3600) return Math.floor(sec/3600) + ' ч ' + Math.floor((sec%3600)/60) + ' мин'; + if (sec >= 60) return Math.floor(sec/60) + ' мин'; + return sec + ' с'; +} + async function pollTask(id) { try { const r = await fetch('/api/tasks/' + id); @@ -90,12 +108,18 @@ async function pollTask(id) { const t = await r.json(); document.getElementById('progressFill').style.width = t.progress + '%'; document.getElementById('progressMsg').textContent = t.message || '…'; + + const speed = fmtSpeed(t.speed_bps); + const eta = fmtETA(t.eta_sec); + const meta = [speed, eta ? 'ETA: ' + eta : ''].filter(Boolean).join(' · '); + document.getElementById('progressMeta').textContent = meta; if (['success','failed','canceled'].includes(t.status)) { stopTaskPoll(); activeTaskId = null; document.getElementById('btnStart').disabled = false; document.getElementById('btnStart').classList.remove('hidden'); document.getElementById('btnCancel').classList.add('hidden'); document.getElementById('progressPanel').classList.add('hidden'); + document.getElementById('progressMeta').textContent = ''; if (t.status === 'success') toast(t.message || 'Готово', 'ok'); if (t.status === 'failed') toast('Ошибка: ' + t.error, 'error'); if (t.status === 'canceled') toast('Копирование отменено', 'error');