Skip to content

Commit c5faabc

Browse files
committed
feat(telegram): auto-cleanup downloaded media files after agent turn
Adds CleanupMedia(maxAge) that removes media files older than maxAge from ~/.odek/media/. Runs in background goroutine after each agent turn with 1-hour max age, since files are only needed during the active turn. 5 tests: removes old, keeps recent, empty dir, missing dir, ignores subdirs
1 parent 4e504ee commit c5faabc

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

cmd/odek/telegram.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,17 @@ func handleChatMessage(
964964
log.Debug("skill suggestions suppressed by skip list", "count", skipped)
965965
}
966966
}
967+
968+
// ── Media cleanup ────────────────────────────────────────────────
969+
// Remove downloaded media files older than 1 hour in the background.
970+
// These are only needed during the agent turn for analysis/transcription.
971+
go func() {
972+
if removed, err := telegram.CleanupMedia(1 * time.Hour); err != nil {
973+
fmt.Fprintf(os.Stderr, "odek telegram: media cleanup: %v\n", err)
974+
} else if removed > 0 {
975+
log.Debug("cleaned up old media files", "count", removed)
976+
}
977+
}()
967978
}
968979

969980
// formatStats formats session statistics for the Telegram stats command.

internal/telegram/download.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"time"
78
)
89

910
// ── Media Directory ────────────────────────────────────────────────────────
@@ -119,3 +120,48 @@ func DownloadPhoto(bot *Bot, fileIDs []string) (string, error) {
119120

120121
return localPath, nil
121122
}
123+
124+
// ── Media Cleanup ──────────────────────────────────────────────────────────
125+
126+
// CleanupMedia removes media files older than maxAge from the downloaded
127+
// media directory (~/.odek/media/). Returns the number of files removed.
128+
// Non-existent directories and subdirectories are silently skipped.
129+
func CleanupMedia(maxAge time.Duration) (int, error) {
130+
dir, err := MediaDir()
131+
if err != nil {
132+
return 0, err
133+
}
134+
135+
entries, err := os.ReadDir(dir)
136+
if err != nil {
137+
if os.IsNotExist(err) {
138+
return 0, nil // nothing to clean
139+
}
140+
return 0, fmt.Errorf("telegram cleanup: read dir: %w", err)
141+
}
142+
143+
cutoff := time.Now().Add(-maxAge)
144+
var removed int
145+
146+
for _, e := range entries {
147+
if e.IsDir() {
148+
continue // skip subdirectories
149+
}
150+
151+
info, err := e.Info()
152+
if err != nil {
153+
continue // skip unreadable entries
154+
}
155+
156+
if info.ModTime().Before(cutoff) {
157+
path := filepath.Join(dir, e.Name())
158+
if err := os.Remove(path); err != nil {
159+
// Log but don't fail — one bad file shouldn't block cleanup.
160+
continue
161+
}
162+
removed++
163+
}
164+
}
165+
166+
return removed, nil
167+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package telegram
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestCleanupMedia_RemovesOldFiles(t *testing.T) {
11+
tmp := t.TempDir()
12+
mediaDir := filepath.Join(tmp, ".odek", "media")
13+
if err := os.MkdirAll(mediaDir, 0700); err != nil {
14+
t.Fatalf("mkdir: %v", err)
15+
}
16+
t.Setenv("HOME", tmp)
17+
18+
// Create an old file (2 hours ago).
19+
oldPath := filepath.Join(mediaDir, "voice_oldfile.oga")
20+
if err := os.WriteFile(oldPath, []byte("old-data"), 0600); err != nil {
21+
t.Fatalf("write old: %v", err)
22+
}
23+
oldTime := time.Now().Add(-2 * time.Hour)
24+
if err := os.Chtimes(oldPath, oldTime, oldTime); err != nil {
25+
t.Fatalf("chtimes old: %v", err)
26+
}
27+
28+
// Create a recent file (10 minutes ago).
29+
recentPath := filepath.Join(mediaDir, "photo_newfile.jpg")
30+
if err := os.WriteFile(recentPath, []byte("new-data"), 0600); err != nil {
31+
t.Fatalf("write new: %v", err)
32+
}
33+
recentTime := time.Now().Add(-10 * time.Minute)
34+
if err := os.Chtimes(recentPath, recentTime, recentTime); err != nil {
35+
t.Fatalf("chtimes new: %v", err)
36+
}
37+
38+
// Clean up files older than 1 hour.
39+
removed, err := CleanupMedia(1 * time.Hour)
40+
if err != nil {
41+
t.Fatalf("CleanupMedia: %v", err)
42+
}
43+
if removed != 1 {
44+
t.Errorf("expected 1 removed file, got %d", removed)
45+
}
46+
47+
// Old file should be gone.
48+
if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
49+
t.Error("old file should have been removed")
50+
}
51+
52+
// Recent file should still exist.
53+
if _, err := os.Stat(recentPath); err != nil {
54+
t.Error("recent file should still exist")
55+
}
56+
}
57+
58+
func TestCleanupMedia_EmptyDir(t *testing.T) {
59+
tmp := t.TempDir()
60+
mediaDir := filepath.Join(tmp, ".odek", "media")
61+
if err := os.MkdirAll(mediaDir, 0700); err != nil {
62+
t.Fatalf("mkdir: %v", err)
63+
}
64+
t.Setenv("HOME", tmp)
65+
66+
removed, err := CleanupMedia(1 * time.Hour)
67+
if err != nil {
68+
t.Fatalf("CleanupMedia on empty dir: %v", err)
69+
}
70+
if removed != 0 {
71+
t.Errorf("expected 0 removed from empty dir, got %d", removed)
72+
}
73+
}
74+
75+
func TestCleanupMedia_DirNotExist(t *testing.T) {
76+
tmp := t.TempDir()
77+
t.Setenv("HOME", tmp)
78+
79+
// Media dir doesn't exist yet — should not error.
80+
removed, err := CleanupMedia(1 * time.Hour)
81+
if err != nil {
82+
t.Fatalf("CleanupMedia on non-existent dir: %v", err)
83+
}
84+
if removed != 0 {
85+
t.Errorf("expected 0 removed, got %d", removed)
86+
}
87+
}
88+
89+
func TestCleanupMedia_AllFilesRecent(t *testing.T) {
90+
tmp := t.TempDir()
91+
mediaDir := filepath.Join(tmp, ".odek", "media")
92+
if err := os.MkdirAll(mediaDir, 0700); err != nil {
93+
t.Fatalf("mkdir: %v", err)
94+
}
95+
t.Setenv("HOME", tmp)
96+
97+
// Create 3 recent files.
98+
for i := 0; i < 3; i++ {
99+
name := filepath.Join(mediaDir, "recent_"+string(rune('a'+i))+".ogg")
100+
if err := os.WriteFile(name, []byte("data"), 0600); err != nil {
101+
t.Fatalf("write: %v", err)
102+
}
103+
}
104+
105+
removed, err := CleanupMedia(1 * time.Hour)
106+
if err != nil {
107+
t.Fatalf("CleanupMedia: %v", err)
108+
}
109+
if removed != 0 {
110+
t.Errorf("expected 0 removed (all recent), got %d", removed)
111+
}
112+
}
113+
114+
func TestCleanupMedia_NonMediaFilesIgnored(t *testing.T) {
115+
tmp := t.TempDir()
116+
mediaDir := filepath.Join(tmp, ".odek", "media")
117+
if err := os.MkdirAll(mediaDir, 0700); err != nil {
118+
t.Fatalf("mkdir: %v", err)
119+
}
120+
t.Setenv("HOME", tmp)
121+
122+
// Create a subdirectory — should be skipped.
123+
subDir := filepath.Join(mediaDir, "subdir")
124+
if err := os.MkdirAll(subDir, 0700); err != nil {
125+
t.Fatalf("mkdir subdir: %v", err)
126+
}
127+
128+
// Create an old file.
129+
oldPath := filepath.Join(mediaDir, "old.oga")
130+
if err := os.WriteFile(oldPath, []byte("data"), 0600); err != nil {
131+
t.Fatalf("write: %v", err)
132+
}
133+
oldTime := time.Now().Add(-3 * time.Hour)
134+
if err := os.Chtimes(oldPath, oldTime, oldTime); err != nil {
135+
t.Fatalf("chtimes: %v", err)
136+
}
137+
138+
removed, err := CleanupMedia(1 * time.Hour)
139+
if err != nil {
140+
t.Fatalf("CleanupMedia: %v", err)
141+
}
142+
if removed != 1 {
143+
t.Errorf("expected 1 removed, got %d", removed)
144+
}
145+
146+
// Subdirectory should still exist.
147+
if _, err := os.Stat(subDir); err != nil {
148+
t.Error("subdirectory should not have been removed")
149+
}
150+
}

0 commit comments

Comments
 (0)