mirror of
https://github.com/tiennm99/telegram-exporter.git
synced 2026-10-11 03:13:49 +00:00
Verification matched on name and non-zero size, so an upload that died partway was counted archived permanently. Comparing against the size Telegram reports found six such objects in the live archive, one of them 221 MiB standing in for a 2006 MiB video. They are folded into the outstanding set; rclone overwrites a size mismatch, so another pass repairs them. A zero Report claimed the archive was complete — nothing expected, nothing missing — which is the value both commands hold before their Telegram callback populates it, so any early return printed COMPLETE and exited 0 on an untouched chat. A Report now knows whether it ran. A name that can never be written kept the run outstanding forever while the download set deliberately excluded it, so a driver looping on "incomplete" walked the whole history and re-indexed the whole remote every pass for work that could not be done. Such a run now reports STALLED and exits 4. A destination that stopped accepting uploads was reported and then discarded, exiting 1. The same driver would retry against a full or unreachable remote indefinitely, downloading gigabytes each pass to upload none. It exits 3. Free space is re-checked during the run, not only before it. An archive this size runs for hours, and the remote can fill in the middle; discovering it through five failed multi-gigabyte uploads wastes the download for all of them. Also: parseSize silently wrapped to a negative or zero on a large input, which reads downstream as "no cap"; list printed attacker-chosen filenames raw, so a tab shifted the columns and an escape sequence reached the terminal; a missing backend blamed credentials rather than the build; humanBytes indexed past its unit table above 1 PiB; a second Init reported success against a config that never loaded; and sync did not surface the basename collisions verify warned about, though sync is the command that acts on the verdict. The env-override test could not observe what it claimed: rclone reads RCLONE_* at package init, so t.Setenv came too late and the assertion held with the guard removed. It runs in a subprocess now, as does the new one covering the index against inherited filters.
163 lines
5.3 KiB
Go
163 lines
5.3 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
)
|
|
|
|
// Resolve's own validation runs before rclone is consulted, so these cases are
|
|
// checkable without a config file or a network.
|
|
func TestResolveRejectsMalformedDestinations(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
remote string
|
|
want string
|
|
}{
|
|
{"empty", "", "destination remote is required"},
|
|
{"no colon", "pikpak", "not in rclone REMOTE:PATH form"},
|
|
{"path only", "/tmp/staging", "not in rclone REMOTE:PATH form"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := Resolve(context.Background(), tt.remote)
|
|
if err == nil {
|
|
t.Fatalf("Resolve(%q) succeeded, want an error", tt.remote)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.want) {
|
|
t.Errorf("Resolve(%q) error = %v, want it to mention %q", tt.remote, err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Forms rclone accepts must not be rejected by our own pre-checks. An earlier
|
|
// version screened the name against the config file's sections, which turned
|
|
// away env-defined remotes, connection strings, and parameterised names that
|
|
// rclone resolves perfectly well. These must get past our validation and fail —
|
|
// if at all — inside rclone, on their own merits.
|
|
func TestResolveDefersUnusualFormsToRclone(t *testing.T) {
|
|
forms := []string{
|
|
"pikpak,chunk_size=10M:mychannel", // parameterised remote name
|
|
":local:/tmp", // inline connection string
|
|
"envonly:bucket", // possibly defined by RCLONE_CONFIG_ENVONLY_*
|
|
}
|
|
|
|
for _, form := range forms {
|
|
t.Run(form, func(t *testing.T) {
|
|
_, err := Resolve(context.Background(), form)
|
|
if err == nil {
|
|
return // rclone resolved it; nothing to assert
|
|
}
|
|
if strings.Contains(err.Error(), "not in rclone REMOTE:PATH form") {
|
|
t.Errorf("Resolve(%q) was rejected by our own syntax check; "+
|
|
"rclone should be the authority on what resolves: %v", form, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultTunablesMatchPikpakSettings(t *testing.T) {
|
|
// These two numbers are the outcome of debugging pikpak's async-commit
|
|
// behaviour in the shell pipeline; a silent change would reintroduce
|
|
// transfers that fail while the server is still committing.
|
|
got := DefaultTunables()
|
|
if got.Transfers != 2 {
|
|
t.Errorf("Transfers = %d, want 2", got.Transfers)
|
|
}
|
|
if got.LowLevelRetries != 20 {
|
|
t.Errorf("LowLevelRetries = %d, want 20", got.LowLevelRetries)
|
|
}
|
|
}
|
|
|
|
func TestInitAppliesTunables(t *testing.T) {
|
|
// Unset both so the defaults, not an operator override, are what lands.
|
|
unset(t, "RCLONE_TRANSFERS")
|
|
unset(t, "RCLONE_LOW_LEVEL_RETRIES")
|
|
|
|
ctx, err := Init(context.Background(), Tunables{Transfers: 2, LowLevelRetries: 20})
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
ci := fs.GetConfig(ctx)
|
|
if ci.Transfers != 2 {
|
|
t.Errorf("ctx Transfers = %d, want 2", ci.Transfers)
|
|
}
|
|
if ci.LowLevelRetries != 20 {
|
|
t.Errorf("ctx LowLevelRetries = %d, want 20", ci.LowLevelRetries)
|
|
}
|
|
}
|
|
|
|
// rclone reads RCLONE_TRANSFERS into its global config at package init, so an
|
|
// in-process t.Setenv cannot observe whether Init honours it — the assertion
|
|
// would pass with the guard removed. A subprocess is the only real check.
|
|
func TestInitLeavesEnvOverridesAlone(t *testing.T) {
|
|
if os.Getenv("GO_INIT_ENV_CHILD") == "1" {
|
|
ctx, err := Init(t.Context(), Tunables{Transfers: 2, LowLevelRetries: 20})
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
ci := fs.GetConfig(ctx)
|
|
if ci.Transfers != 7 {
|
|
t.Errorf("Transfers = %d, want the operator's 7", ci.Transfers)
|
|
}
|
|
if ci.LowLevelRetries != 99 {
|
|
t.Errorf("LowLevelRetries = %d, want the operator's 99", ci.LowLevelRetries)
|
|
}
|
|
return
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestInitLeavesEnvOverridesAlone", "-test.v")
|
|
cmd.Env = append(os.Environ(),
|
|
"GO_INIT_ENV_CHILD=1",
|
|
"RCLONE_TRANSFERS=7",
|
|
"RCLONE_LOW_LEVEL_RETRIES=99",
|
|
)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Errorf("Init overrode the operator's environment:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// And with nothing set, the pikpak-derived tunables are what apply.
|
|
func TestInitAppliesTunablesWhenTheEnvIsQuiet(t *testing.T) {
|
|
if os.Getenv("GO_INIT_QUIET_CHILD") == "1" {
|
|
ctx, err := Init(t.Context(), Tunables{Transfers: 2, LowLevelRetries: 20})
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
ci := fs.GetConfig(ctx)
|
|
if ci.Transfers != 2 || ci.LowLevelRetries != 20 {
|
|
t.Errorf("Transfers=%d LowLevelRetries=%d, want 2 and 20",
|
|
ci.Transfers, ci.LowLevelRetries)
|
|
}
|
|
return
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestInitAppliesTunablesWhenTheEnvIsQuiet", "-test.v")
|
|
cmd.Env = append(os.Environ(), "GO_INIT_QUIET_CHILD=1")
|
|
// Cleared rather than assumed absent: the parent's own environment may
|
|
// carry them, which would make this assert the opposite of what it says.
|
|
cmd.Env = slices.DeleteFunc(cmd.Env, func(kv string) bool {
|
|
return strings.HasPrefix(kv, "RCLONE_TRANSFERS=") ||
|
|
strings.HasPrefix(kv, "RCLONE_LOW_LEVEL_RETRIES=")
|
|
})
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Errorf("Init did not apply its tunables:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// unset removes a variable for the duration of the test, restoring it after.
|
|
func unset(t *testing.T, key string) {
|
|
t.Helper()
|
|
if old, ok := os.LookupEnv(key); ok {
|
|
t.Cleanup(func() { _ = os.Setenv(key, old) })
|
|
}
|
|
_ = os.Unsetenv(key)
|
|
}
|