diff --git a/internal/telegram/files.go b/internal/telegram/files.go index b9dbd46..1085fb5 100644 --- a/internal/telegram/files.go +++ b/internal/telegram/files.go @@ -160,8 +160,8 @@ func (c *Client) DownloadFileSync(key string) (*File, error) { } name := fmt.Sprintf("%s_%s", - strings.NewReplacer(":", "_", "/", "_").Replace(key), - filepath.Base(entry.name)) + sanitizeDownloadFileName(key), + sanitizeDownloadFileName(entry.name)) path := filepath.Join(c.config.Storage.FilesDir, name) ctx := context.Background() @@ -175,6 +175,24 @@ func (c *Client) DownloadFileSync(key string) (*File, error) { return file, nil } +// sanitizeDownloadFileName makes Telegram-provided names safe on Windows too. +// Photo registry names contain ':' by design, and document names are remote +// input, so both halves of the generated local name must be sanitized. +func sanitizeDownloadFileName(name string) string { + name = filepath.Base(name) + name = strings.Map(func(r rune) rune { + if r < 32 || strings.ContainsRune(`<>:"/\|?*`, r) { + return '_' + } + return r + }, name) + name = strings.TrimRight(name, " .") + if name == "" { + return "download" + } + return name +} + // inputPeer resolves a canonical chat ID to a tg InputPeer // via the peers manager (handles access hashes). func (c *Client) inputPeer(ctx context.Context, chatID int64) (tg.InputPeerClass, error) { diff --git a/internal/telegram/files_test.go b/internal/telegram/files_test.go new file mode 100644 index 0000000..018bd0f --- /dev/null +++ b/internal/telegram/files_test.go @@ -0,0 +1,16 @@ +package telegram + +import "testing" + +func TestSanitizeDownloadFileName(t *testing.T) { + tests := map[string]string{ + "photo:5211047124492479647:y.jpg": "photo_5211047124492479647_y.jpg", + `question<1>:"draft"?.pdf`: "question_1___draft__.pdf", + "trailing. ": "trailing", + } + for input, want := range tests { + if got := sanitizeDownloadFileName(input); got != want { + t.Errorf("sanitizeDownloadFileName(%q) = %q, want %q", input, got, want) + } + } +}