Files
tiennm99 b05247abed fix: retry a failed upload instead of abandoning the file
pikpak commits an upload as a server-side async task, and rclone polls that
task only as long as its low-level retries last. A task still in
PHASE_TYPE_PENDING when that budget runs out is reported as "can't verify the
task is completed" — and checking the live archive ten minutes later, those
tasks had not committed: the objects were simply absent.

The transfer itself was fine; only the confirmation timed out. Abandoning the
file meant re-downloading it from Telegram on the next pass, which for this
archive can be two gigabytes, so a run against a slow remote paid for the same
bytes repeatedly. The staged copy is still on disk when a move fails — rclone
says so in the same breath — so another attempt costs seconds instead.

Every attempt after the first checks the remote before re-uploading. A pending
task may have committed during the backoff, and pikpak allows two files under
one name, so re-uploading blind is how one file becomes two — which
verification then reports as an ambiguous basename on every future run.

rclone's log now goes through the renderer. It writes to stderr on its own
schedule, so its error lines were landing mid-redraw and shredding the display
exactly when there was most to read.

Also: "capped at uncapped" now reads "uncapped", and the ETA column fits the
three-digit hour counts a slow remote produces.
2026-09-06 21:54:38 +07:00

62 lines
1.9 KiB
Go

package report
import (
"fmt"
"io"
"github.com/tiennm99dev/telegram-exporter/internal/verify"
)
// writeSurvey states what the chat holds and how much of it is already archived,
// broken out by why each outstanding file is outstanding. The single "N to
// fetch" it replaces hid the difference between never-fetched, empty,
// wrong-size, and unarchivable — which is the difference between a run that will
// converge and one that cannot.
func Survey(w io.Writer, r verify.Report) {
fmt.Fprintf(w, "\n chat holds %s media, %s\n",
humanCount(r.Expected), humanBytes(r.Bytes))
fmt.Fprintf(w, " archived %s\n", humanCount(r.Present))
for _, row := range []struct {
label string
n int
}{
{"never fetched", len(r.Absent) - len(r.Unsafe)},
{"zero-byte", len(r.ZeroByte)},
{"wrong size", len(r.Mismatched)},
{"unarchivable", len(r.Unsafe)},
} {
if row.n > 0 {
fmt.Fprintf(w, " %-13s %s\n", row.label, humanCount(row.n))
}
}
}
// PlanInfo is what the run is about to do, stated before it starts.
type PlanInfo struct {
Files int
Bytes int64
Largest int64
Budget int64
Staging string
Threads int
Downloads, Uploads int
Destination string
}
// writePlan prints the settings that decide how long the run takes and how much
// disk it uses, so an operator can stop it before a multi-hour transfer rather
// than discover the wrong cap partway through.
func Plan(w io.Writer, p PlanInfo) {
cap := "uncapped"
if p.Budget > 0 {
cap = "capped at " + humanBytes(p.Budget)
}
fmt.Fprintf(w, "\n fetching %s files, %s (largest %s)\n",
humanCount(p.Files), humanBytes(p.Bytes), humanBytes(p.Largest))
fmt.Fprintf(w, " into %s\n", p.Destination)
fmt.Fprintf(w, " staging %s, %s\n", p.Staging, cap)
fmt.Fprintf(w, " concurrency %d download(s) x %d thread(s), %d upload(s)\n\n",
p.Downloads, p.Threads, p.Uploads)
}