diff --git a/README.md b/README.md index b85c8f6..f7d95fc 100644 --- a/README.md +++ b/README.md @@ -204,16 +204,6 @@ semaphore. Some hard-won details were worth keeping, and are: `RCLONE_MIN_SIZE` left over from another job would otherwise narrow the index and re-download everything it hid. -Flags that disappeared are recognised and explain what replaced them: - -| Old | Why it is gone | -|---|---| -| `-i` | no sweep interval; uploads start when a download finishes | -| `-a` | no `--min-age`; completion is observed, not inferred | -| `-f` | no export JSON; the chat is read live, so names cannot go stale | -| `-p` | no passes; one invocation converges | -| `-q` | renamed `--min-free` | - ## Notes - `tgexport` and the `tdl` CLI share one session store and cannot run against the diff --git a/cmd/tgexport/doctor.go b/cmd/tgexport/doctor.go index f39ceb4..9a4ebcb 100644 --- a/cmd/tgexport/doctor.go +++ b/cmd/tgexport/doctor.go @@ -27,10 +27,13 @@ import ( func doctorCmd(ctx context.Context, args []string) error { fs := flag.NewFlagSet("doctor", flag.ContinueOnError) var ( - remoteArg = fs.String("r", "", "rclone destination to check, e.g. pikpak:archive") - ns = fs.String("n", "default", "tdl session namespace") - dataDir = fs.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + remoteArg = fs.String("r", "", "rclone destination `REMOTE:PATH` to check, e.g. pikpak:archive") + ns = fs.String("n", "default", "tdl session `NAMESPACE`") + dataDir = fs.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") ) + + commandUsage(fs, "tgexport doctor [-r REMOTE:PATH] [options]", + "Check the Telegram session and, when a remote is given, that it\nresolves and has free space. Run this before a long archive.") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return err // main maps this to a clean exit diff --git a/cmd/tgexport/list.go b/cmd/tgexport/list.go index 5cdccb0..e8cb7e7 100644 --- a/cmd/tgexport/list.go +++ b/cmd/tgexport/list.go @@ -28,10 +28,13 @@ import ( func listCmd(ctx context.Context, args []string) error { fs := flag.NewFlagSet("list", flag.ContinueOnError) var ( - chat = fs.String("c", "", "chat id, username, or t.me link (required)") - ns = fs.String("n", "default", "tdl session namespace") - dataDir = fs.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + chat = fs.String("c", "", "`CHAT`: id, username, or t.me link (required)") + ns = fs.String("n", "default", "tdl session `NAMESPACE`") + dataDir = fs.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") ) + + commandUsage(fs, "tgexport list -c CHAT [options]", + "Print every media message in a chat as idsizename.") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return err diff --git a/cmd/tgexport/main.go b/cmd/tgexport/main.go index 582e3b6..2107d18 100644 --- a/cmd/tgexport/main.go +++ b/cmd/tgexport/main.go @@ -162,10 +162,29 @@ func usage() { Commands: sync Archive a chat to a remote, fetching only what is missing - list Print every media message in a chat as idsizename verify Report whether a chat is fully archived on a remote + list Print every media message in a chat as idsizename doctor Check the Telegram session, the destination remote, and free space +Exit codes: + 0 complete 2 usage error 4 stalled: nothing left is fetchable + 1 files remain 3 remote or Telegram failure + 130/143 interrupted + +Only 1 is worth retrying; a driver looping until 0 should stop on anything else. + Run 'tgexport -h' for command options. `) } + +// commandUsage gives a subcommand a header its flag list can hang off. +// +// The flag package's default is "Usage of sync:" and a bare list, which says +// neither what the command does nor which options are required. +func commandUsage(fs *flag.FlagSet, line, summary string) { + fs.Usage = func() { + out := fs.Output() + fmt.Fprintf(out, "Usage: %s\n\n%s\n\nOptions:\n", line, summary) + fs.PrintDefaults() + } +} diff --git a/cmd/tgexport/sync.go b/cmd/tgexport/sync.go index 5360b10..90bdd94 100644 --- a/cmd/tgexport/sync.go +++ b/cmd/tgexport/sync.go @@ -21,41 +21,28 @@ import ( "github.com/tiennm99dev/telegram-exporter/internal/verify" ) -// retiredFlags map options the shell pipeline had onto what replaced them. -// -// Recognising them beats "flag provided but not defined": these were in -// muscle memory and in wrapper scripts, and a bare parse error does not say -// whether the concept moved or disappeared. -var retiredFlags = map[string]string{ - "i": "the rclone sweep interval is gone; uploads start the moment a download finishes", - "a": "--min-age is gone; a file is only uploaded once the downloader reports it complete", - "f": "the export JSON is gone; the chat is read live, so names cannot go stale", - "p": "there are no passes; one invocation converges, and re-running resumes", - "q": "renamed to --min-free", -} - // syncCmd archives a chat to a remote: read the chat, skip what is already // there, download and upload the rest, then report on the result. func syncCmd(ctx context.Context, args []string) error { flags := flag.NewFlagSet("sync", flag.ContinueOnError) var ( - chat = flags.String("c", "", "chat id, username, or t.me link (required)") - remoteArg = flags.String("r", "", "rclone destination, e.g. pikpak:archive (required)") - staging = flags.String("d", "./staging", "staging directory for files in flight") - maxStaging = flags.String("m", "", "cap staging at this size, e.g. 40G (default: no cap)") + chat = flags.String("c", "", "`CHAT`: id, username, or t.me link (required)") + remoteArg = flags.String("r", "", "rclone destination `REMOTE:PATH`, e.g. pikpak:archive (required)") + staging = flags.String("d", "./staging", "staging `DIR` for files in flight") + maxStaging = flags.String("m", "", "cap staging at `SIZE`, e.g. 40G (default: no cap)") threads = flags.Int("threads", 4, "connections per file") limit = flags.Int("limit", 2, "files downloading at once") uploads = flags.Int("uploads", 2, "files uploading at once") - minFree = flags.Int64("min-free", 5, "stop if the remote has fewer than this many GiB free") + minFree = flags.Int64("min-free", 5, "stop when the remote has under this many `GiB` free") limitItems = flags.Int("limit-items", 0, "stop after this many files (0 means no limit)") confirm = flags.Bool("confirm", true, "re-state each uploaded file to prove its size") - takeout = flags.Bool("takeout", true, "use a takeout session, as `tdl dl --takeout` did") - ns = flags.String("n", "default", "tdl session namespace") - dataDir = flags.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + takeout = flags.Bool("takeout", true, "use a takeout session for higher rate limits") + ns = flags.String("n", "default", "tdl session `NAMESPACE`") + dataDir = flags.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") ) - for name, replacement := range retiredFlags { - flags.Var(retiredFlag{name, replacement}, name, "retired") - } + commandUsage(flags, "tgexport sync -c CHAT -r REMOTE:PATH [options]", + "Archive a chat's media to a remote, fetching only what is missing.\n"+ + "Re-running resumes: anything already on the remote is skipped.") if err := flags.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { @@ -353,14 +340,6 @@ func checkFree(ctx context.Context, dst fs.Fs, minGiB int64) error { return nil } -// retiredFlag reports a helpful error for an option that no longer exists. -type retiredFlag struct{ name, replacement string } - -func (r retiredFlag) String() string { return "" } -func (r retiredFlag) Set(string) error { - return fmt.Errorf("-%s no longer exists: %s", r.name, r.replacement) -} - // parseSize reads a binary size such as 40G, matching what run.sh -m accepted. func parseSize(s string) (int64, error) { if s == "" { diff --git a/cmd/tgexport/sync_test.go b/cmd/tgexport/sync_test.go index 9cd6d10..d9dd56c 100644 --- a/cmd/tgexport/sync_test.go +++ b/cmd/tgexport/sync_test.go @@ -112,28 +112,3 @@ func TestValidateBudgetRejectsCapBelowLargestFile(t *testing.T) { t.Errorf("an unset cap should accept anything, got: %v", err) } } - -// Options the shell pipeline had must produce an explanation, not "flag -// provided but not defined" — they are in wrapper scripts and muscle memory. -func TestRetiredFlagsExplainWhatReplacedThem(t *testing.T) { - for name, replacement := range retiredFlags { - err := retiredFlag{name, replacement}.Set("x") - if err == nil { - t.Errorf("-%s was accepted, want an explanation", name) - continue - } - if !strings.Contains(err.Error(), "-"+name) { - t.Errorf("error for -%s should name the flag, got: %v", name, err) - } - if !strings.Contains(err.Error(), replacement) { - t.Errorf("error for -%s should say what replaced it, got: %v", name, err) - } - } - - // The ones that mattered most in run.sh. - for _, name := range []string{"i", "a", "f", "p", "q"} { - if _, ok := retiredFlags[name]; !ok { - t.Errorf("-%s was a run.sh flag but is not recognised as retired", name) - } - } -} diff --git a/cmd/tgexport/verify.go b/cmd/tgexport/verify.go index 8239e47..b6b396d 100644 --- a/cmd/tgexport/verify.go +++ b/cmd/tgexport/verify.go @@ -29,13 +29,16 @@ import ( func verifyCmd(ctx context.Context, args []string) error { fs := flag.NewFlagSet("verify", flag.ContinueOnError) var ( - chat = fs.String("c", "", "chat id, username, or t.me link (required)") - remoteArg = fs.String("r", "", "rclone destination, e.g. pikpak:archive (required)") - ns = fs.String("n", "default", "tdl session namespace") - dataDir = fs.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + chat = fs.String("c", "", "`CHAT`: id, username, or t.me link (required)") + remoteArg = fs.String("r", "", "rclone destination `REMOTE:PATH`, e.g. pikpak:archive (required)") + ns = fs.String("n", "default", "tdl session `NAMESPACE`") + dataDir = fs.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") delStale = fs.Bool("delete-misnamed", false, "delete remote files stored under a superseded name") assumeYes = fs.Bool("y", false, "do not prompt before deleting") ) + + commandUsage(fs, "tgexport verify -c CHAT -r REMOTE:PATH [options]", + "Compare a chat against a remote and report what is missing, empty,\nthe wrong size, or stored under a name that cannot be written.") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return err