Skip to content

Commit 0cf2e17

Browse files
AnnatarHeclaude
andcommitted
refactor(gc): use path helpers and add automatic large log cleanup
- Replace os.ExpandEnv path construction with model.GetXxxPath() helpers - Add cleanLogFile() and cleanLargeLogFiles() for size-based cleanup - Add backupAndWriteFile() helper for consistent backup pattern - Extract cleanCommandFiles() for better code organization - Auto-clean log files >50MB; --withLog forces cleanup regardless of size 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 03f7962 commit 0cf2e17

1 file changed

Lines changed: 114 additions & 57 deletions

File tree

commands/gc.go

Lines changed: 114 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
67
"log/slog"
78
"os"
@@ -12,6 +13,8 @@ import (
1213
"go.opentelemetry.io/otel/trace"
1314
)
1415

16+
const logFileSizeThreshold int64 = 50 * 1024 * 1024 // 50 MB
17+
1518
var GCCommand *cli.Command = &cli.Command{
1619
Name: "gc",
1720
Usage: "clean internal storage",
@@ -31,28 +34,74 @@ var GCCommand *cli.Command = &cli.Command{
3134
Action: commandGC,
3235
}
3336

34-
func commandGC(c *cli.Context) error {
35-
ctx, span := commandTracer.Start(c.Context, "gc", trace.WithSpanKind(trace.SpanKindClient))
36-
defer span.End()
37-
storageFolder := os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER)
38-
if _, err := os.Stat(storageFolder); os.IsNotExist(err) {
39-
return nil
37+
// cleanLogFile removes a log file if it exceeds the threshold or if force is true.
38+
// Returns the size of the deleted file (0 if not deleted or file doesn't exist).
39+
func cleanLogFile(filePath string, threshold int64, force bool) (int64, error) {
40+
info, err := os.Stat(filePath)
41+
if os.IsNotExist(err) {
42+
return 0, nil
43+
}
44+
if err != nil {
45+
return 0, fmt.Errorf("failed to stat file %s: %w", filePath, err)
46+
}
47+
48+
fileSize := info.Size()
49+
if !force && fileSize < threshold {
50+
return 0, nil
4051
}
4152

42-
if c.Bool("withLog") {
43-
logFile := os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER + "/log.log")
44-
if err := os.Remove(logFile); err != nil && !os.IsNotExist(err) {
45-
return fmt.Errorf("failed to remove log file: %v", err)
53+
if err := os.Remove(filePath); err != nil {
54+
return 0, fmt.Errorf("failed to remove file %s: %w", filePath, err)
55+
}
56+
57+
slog.Info("cleaned log file", slog.String("file", filePath), slog.Int64("size_bytes", fileSize))
58+
return fileSize, nil
59+
}
60+
61+
// cleanLargeLogFiles checks all log files and removes those exceeding the size threshold.
62+
// If force is true, removes all log files regardless of size.
63+
func cleanLargeLogFiles(force bool) (int64, error) {
64+
logFiles := []string{
65+
model.GetLogFilePath(),
66+
model.GetHeartbeatLogFilePath(),
67+
model.GetSyncPendingFilePath(),
68+
}
69+
70+
var totalFreed int64
71+
for _, filePath := range logFiles {
72+
freed, err := cleanLogFile(filePath, logFileSizeThreshold, force)
73+
if err != nil {
74+
slog.Warn("failed to clean log file", slog.String("file", filePath), slog.Any("err", err))
75+
continue
4676
}
77+
totalFreed += freed
4778
}
4879

49-
if !c.Bool("skipLogCreation") {
50-
// only can setup logger after the log file clean
51-
SetupLogger(storageFolder)
52-
defer CloseLogger()
80+
return totalFreed, nil
81+
}
82+
83+
// backupAndWriteFile backs up the existing file and writes new content.
84+
func backupAndWriteFile(filePath string, content []byte) error {
85+
backupFile := filePath + ".bak"
86+
87+
if _, err := os.Stat(filePath); err == nil {
88+
if err := os.Rename(filePath, backupFile); err != nil {
89+
slog.Warn("failed to backup file", slog.String("file", filePath), slog.Any("err", err))
90+
return fmt.Errorf("failed to backup file %s: %w", filePath, err)
91+
}
92+
}
93+
94+
if err := os.WriteFile(filePath, content, 0644); err != nil {
95+
slog.Warn("failed to write file", slog.String("file", filePath), slog.Any("err", err))
96+
return fmt.Errorf("failed to write file %s: %w", filePath, err)
5397
}
5498

55-
commandsFolder := os.ExpandEnv("$HOME/" + model.COMMAND_STORAGE_FOLDER)
99+
return nil
100+
}
101+
102+
// cleanCommandFiles cleans up the command storage files based on the cursor position.
103+
func cleanCommandFiles(ctx context.Context) error {
104+
commandsFolder := model.GetCommandsStoragePath()
56105
if _, err := os.Stat(commandsFolder); os.IsNotExist(err) {
57106
return nil
58107
}
@@ -133,66 +182,74 @@ func commandGC(c *cli.Context) error {
133182
)
134183
})
135184

136-
originalPreFile := os.ExpandEnv("$HOME/" + model.COMMAND_PRE_STORAGE_FILE)
137-
originalPostFile := os.ExpandEnv("$HOME/" + model.COMMAND_POST_STORAGE_FILE)
138-
originalCursorFile := os.ExpandEnv("$HOME/" + model.COMMAND_CURSOR_STORAGE_FILE)
139-
140-
preBackupFile := originalPreFile + ".bak"
141-
postBackupFile := originalPostFile + ".bak"
142-
cursorBackupFile := originalCursorFile + ".bak"
143-
144-
if _, err := os.Stat(originalPreFile); err == nil {
145-
if err := os.Rename(originalPreFile, preBackupFile); err != nil {
146-
slog.Warn("failed to backup PRE_FILE", slog.Any("err", err))
147-
return fmt.Errorf("failed to backup PRE_FILE: %v", err)
148-
}
149-
}
150-
151-
if _, err := os.Stat(originalPostFile); err == nil {
152-
if err := os.Rename(originalPostFile, postBackupFile); err != nil {
153-
slog.Warn("failed to backup POST_FILE", slog.Any("err", err))
154-
return fmt.Errorf("failed to backup POST_FILE: %v", err)
155-
}
156-
}
157-
if _, err := os.Stat(originalCursorFile); err == nil {
158-
if err := os.Rename(originalCursorFile, cursorBackupFile); err != nil {
159-
slog.Warn("failed to backup CURSOR_FILE", slog.Any("err", err))
160-
return fmt.Errorf("failed to backup CURSOR_FILE: %v", err)
161-
}
162-
}
163-
185+
// Build pre file content
164186
preFileContent := bytes.Buffer{}
165187
for _, cmd := range newPreCommandList {
166188
line, err := cmd.ToLine(cmd.RecordingTime)
167189
if err != nil {
168-
return fmt.Errorf("failed to convert command to line: %v", err)
190+
return fmt.Errorf("failed to convert command to line: %w", err)
169191
}
170192
preFileContent.Write(line)
171193
}
172-
if err := os.WriteFile(originalPreFile, preFileContent.Bytes(), 0644); err != nil {
173-
slog.Warn("failed to write new PRE_FILE", slog.Any("err", err))
174-
return fmt.Errorf("failed to write new PRE_FILE: %v", err)
175-
}
176194

195+
// Build post file content
177196
postFileContent := bytes.Buffer{}
178197
for _, cmd := range newPostCommandList {
179198
line, err := cmd.ToLine(cmd.RecordingTime)
180199
if err != nil {
181-
return fmt.Errorf("failed to convert command to line: %v", err)
200+
return fmt.Errorf("failed to convert command to line: %w", err)
182201
}
183202
postFileContent.Write(line)
184203
}
185204

186-
if err := os.WriteFile(originalPostFile, postFileContent.Bytes(), 0644); err != nil {
187-
slog.Warn("failed to write new POST_FILE", slog.Any("err", err))
188-
return fmt.Errorf("failed to write new POST_FILE: %v", err)
205+
// Build cursor file content
206+
lastCursorNano := lastCursor.UnixNano()
207+
cursorContent := []byte(fmt.Sprintf("%d", lastCursorNano))
208+
209+
// Backup and write all files
210+
if err := backupAndWriteFile(model.GetPreCommandFilePath(), preFileContent.Bytes()); err != nil {
211+
return err
189212
}
190213

191-
lastCursorNano := lastCursor.UnixNano()
192-
lastCursorBytes := []byte(fmt.Sprintf("%d", lastCursorNano))
193-
if err := os.WriteFile(originalCursorFile, lastCursorBytes, 0644); err != nil {
194-
slog.Warn("failed to write new CURSOR_FILE", slog.Any("err", err))
195-
return fmt.Errorf("failed to write new CURSOR_FILE: %v", err)
214+
if err := backupAndWriteFile(model.GetPostCommandFilePath(), postFileContent.Bytes()); err != nil {
215+
return err
216+
}
217+
218+
if err := backupAndWriteFile(model.GetCursorFilePath(), cursorContent); err != nil {
219+
return err
220+
}
221+
222+
return nil
223+
}
224+
225+
func commandGC(c *cli.Context) error {
226+
ctx, span := commandTracer.Start(c.Context, "gc", trace.WithSpanKind(trace.SpanKindClient))
227+
defer span.End()
228+
229+
storageFolder := model.GetBaseStoragePath()
230+
if _, err := os.Stat(storageFolder); os.IsNotExist(err) {
231+
return nil
232+
}
233+
234+
// Clean log files: force clean if --withLog, otherwise only clean large files
235+
forceCleanLogs := c.Bool("withLog")
236+
freedBytes, err := cleanLargeLogFiles(forceCleanLogs)
237+
if err != nil {
238+
slog.Warn("error during log cleanup", slog.Any("err", err))
239+
}
240+
if freedBytes > 0 {
241+
slog.Info("freed space from log files", slog.Int64("bytes", freedBytes))
242+
}
243+
244+
if !c.Bool("skipLogCreation") {
245+
// only can setup logger after the log file clean
246+
SetupLogger(storageFolder)
247+
defer CloseLogger()
248+
}
249+
250+
// Clean command files
251+
if err := cleanCommandFiles(ctx); err != nil {
252+
return err
196253
}
197254

198255
// TODO: delete $HOME/.config/malamtime/ folder

0 commit comments

Comments
 (0)