@@ -2,6 +2,7 @@ package commands
22
33import (
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+
1518var 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