mirror of
https://github.com/tiennm99/telegram-exporter.git
synced 2026-10-13 12:15:09 +00:00
Second slice: the read path, and the fix for the bug that motivated the rewrite. The shell pipeline derived a filename twice. `tdl chat export` wrote the raw Telegram name into a JSON, while `tdl dl` rendered it through a template whose default pipes it through filenamify, which rewrites reserved characters and collapses runs of '!'. A message whose name contained '!!' was therefore looked up under one name and stored under another; the verifier never found it and re-fetched it on every pass. Here a single function produces the name, and the string it returns is used both to test for presence and to write the file, so the two cannot disagree. Names are stored exactly as Telegram reports them rather than reproducing filenamify. That is a deliberate break from what the old pipeline wrote: a file it stored under a rewritten name is not recognised and will be fetched again. For the one chat archived so far that is a single file out of 18155, already removed. Because names are verbatim they are not path-safe, so the code that turns one into a path must enforce containment. Walk yields a sequence rather than taking a callback, since the downloader consumes a pull iterator and range-over-func converts either way without anyone owning a goroutine. It pages newest first, where the old pipeline went oldest first, which changes what an interrupted run leaves behind. Message links are refused rather than guessed at, across every host Telegram uses and the tg:// forms that carry the message id in a query parameter. A private channel link and a public message link have the same shape, so the two are told apart by parsing rather than by pattern. Verified against the live chat: 18155 media messages, matching the shell verifier, and every one of the 15548 objects already on the remote is found under a derived name.
76 lines
2.3 KiB
Go
76 lines
2.3 KiB
Go
package naming
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// buildsAName matches the ways a stored filename would plausibly be assembled by
|
|
// hand: the format verbs ("%d_%d_%s" and relatives) and string concatenation
|
|
// around a bare "_" separator.
|
|
//
|
|
// This is a lint for known shapes, not a proof. A determined reimplementation —
|
|
// a strings.Builder copy of For, say — still slips through. It catches the
|
|
// realistic accident, which is someone reaching for Sprintf in a new file.
|
|
var buildsAName = regexp.MustCompile(`%[ds]_%[ds]|_%[ds]_|\+ *"_" *\+`)
|
|
|
|
// The whole point of this package is that it is the only answer to "what is this
|
|
// file called". Nothing in the type system prevents a second place from
|
|
// formatting the same string, so the common ways of doing so are linted here.
|
|
//
|
|
// If this test fails, the fix is to call For (or MessageID) rather than to widen
|
|
// the pattern. The shell pipeline's bug was two independent name derivations that
|
|
// nothing forced to agree; a second one here would reintroduce it exactly.
|
|
func TestNamingIsTheSoleSourceOfFilenames(t *testing.T) {
|
|
root, err := filepath.Abs("../..")
|
|
if err != nil {
|
|
t.Fatalf("locate repo root: %v", err)
|
|
}
|
|
|
|
var offenders []string
|
|
err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
// Skip VCS and anything vendored; only our own source counts.
|
|
switch d.Name() {
|
|
case ".git", "vendor", "plans", "staging":
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
// This package is the one place allowed to build the name.
|
|
if filepath.Dir(path) == filepath.Join(root, "internal", "naming") {
|
|
return nil
|
|
}
|
|
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, line := range strings.Split(string(src), "\n") {
|
|
if buildsAName.MatchString(line) {
|
|
rel, _ := filepath.Rel(root, path)
|
|
offenders = append(offenders, rel+":"+strconv.Itoa(i+1)+": "+strings.TrimSpace(line))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk source tree: %v", err)
|
|
}
|
|
|
|
if len(offenders) > 0 {
|
|
t.Errorf("filenames must only be built by naming.For; found %d other place(s):\n %s",
|
|
len(offenders), strings.Join(offenders, "\n "))
|
|
}
|
|
}
|