diff --git a/eget.go b/eget.go index bd46219..da70fa1 100644 --- a/eget.go +++ b/eget.go @@ -365,6 +365,14 @@ func main() { os.Exit(0) } + if cli.ShowLog { + err := PrintLogs() + if err != nil { + fatal(err) + } + os.Exit(0) + } + target := "" if len(args) > 0 { @@ -398,12 +406,19 @@ func main() { if opts.Remove { ebin := os.Getenv("EGET_BIN") - err := os.Remove(filepath.Join(ebin, target)) + removePath := filepath.Join(ebin, target) + err := os.Remove(removePath) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - fmt.Printf("Removed `%s`\n", filepath.Join(ebin, target)) + fmt.Printf("Removed `%s`\n", removePath) + + // Log the removal operation + if logErr := LogOperation(target, removePath, "removed"); logErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to log removal: %v\n", logErr) + } + os.Exit(0) } @@ -547,12 +562,25 @@ func main() { } } + // Check if file already exists (for update detection) + action := "installed" + if _, err := os.Stat(out); err == nil { + action = "updated" + } + err = bin.Extract(out) if err != nil { fatal(err) } fmt.Fprintf(output, "Extracted `%s` to `%s`\n", bin.ArchiveName, out) + + // Log the operation (skip logging to stdout) + if out != "-" { + if logErr := LogOperation(target, out, action); logErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to log operation: %v\n", logErr) + } + } } if opts.All { diff --git a/flags.go b/flags.go index 2fa6596..4b7fe5d 100644 --- a/flags.go +++ b/flags.go @@ -16,6 +16,7 @@ type Flags struct { Verify string Remove bool DisableSSL bool + ShowLog bool } type CliFlags struct { @@ -38,4 +39,5 @@ type CliFlags struct { Help bool `short:"h" long:"help" description:"show this help message"` DownloadAll bool `short:"D" long:"download-all" description:"download all projects defined in the config file"` DisableSSL *bool `short:"k" long:"disable-ssl" description:"disable SSL verification for download requests"` + ShowLog bool `long:"log" description:"show the installation log"` } diff --git a/log.go b/log.go new file mode 100644 index 0000000..831ce2e --- /dev/null +++ b/log.go @@ -0,0 +1,165 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/zyedidia/eget/home" +) + +// LogEntry represents a single log entry for a binary operation +type LogEntry struct { + Timestamp time.Time + Repo string + Path string + Action string +} + +// GetLogDir returns the appropriate log directory based on the OS +func GetLogDir() (string, error) { + var logDir string + + if runtime.GOOS == "windows" { + // Windows: use %LOCALAPPDATA%\eget\logs + localAppData := os.Getenv("LOCALAPPDATA") + if localAppData == "" { + homeDir, err := home.Home() + if err != nil { + return "", fmt.Errorf("could not determine home directory: %w", err) + } + localAppData = filepath.Join(homeDir, "AppData", "Local") + } + logDir = filepath.Join(localAppData, "eget", "logs") + } else { + // Unix-like systems: use ~/.local/share/eget/logs + homeDir, err := home.Home() + if err != nil { + return "", fmt.Errorf("could not determine home directory: %w", err) + } + logDir = filepath.Join(homeDir, ".local", "share", "eget", "logs") + } + + return logDir, nil +} + +// GetLogFilePath returns the full path to the log file +func GetLogFilePath() (string, error) { + logDir, err := GetLogDir() + if err != nil { + return "", err + } + return filepath.Join(logDir, "eget.log"), nil +} + +// ensureLogDir creates the log directory if it doesn't exist +func ensureLogDir() error { + logDir, err := GetLogDir() + if err != nil { + return err + } + + return os.MkdirAll(logDir, 0755) +} + +// LogOperation logs a binary operation to the log file +func LogOperation(repo, path, action string) error { + // Ensure log directory exists + if err := ensureLogDir(); err != nil { + return fmt.Errorf("failed to create log directory: %w", err) + } + + logFile, err := GetLogFilePath() + if err != nil { + return fmt.Errorf("failed to get log file path: %w", err) + } + + // Open file in append mode, create if doesn't exist + f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open log file: %w", err) + } + defer f.Close() + + // Format: timestamp\trepo\tpath\taction + timestamp := time.Now().UTC().Format(time.RFC3339) + logLine := fmt.Sprintf("%s\t%s\t%s\t%s\n", timestamp, repo, path, action) + + _, err = f.WriteString(logLine) + if err != nil { + return fmt.Errorf("failed to write to log file: %w", err) + } + + return nil +} + +// ReadLogs reads all log entries from the log file +func ReadLogs() ([]LogEntry, error) { + logFile, err := GetLogFilePath() + if err != nil { + return nil, err + } + + data, err := os.ReadFile(logFile) + if err != nil { + if os.IsNotExist(err) { + return []LogEntry{}, nil + } + return nil, fmt.Errorf("failed to read log file: %w", err) + } + + lines := strings.Split(string(data), "\n") + entries := make([]LogEntry, 0, len(lines)) + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + parts := strings.Split(line, "\t") + if len(parts) != 4 { + continue // skip malformed lines + } + + timestamp, err := time.Parse(time.RFC3339, parts[0]) + if err != nil { + continue // skip lines with invalid timestamps + } + + entries = append(entries, LogEntry{ + Timestamp: timestamp, + Repo: parts[1], + Path: parts[2], + Action: parts[3], + }) + } + + return entries, nil +} + +// FormatLogEntry formats a log entry for display +func FormatLogEntry(entry LogEntry) string { + return fmt.Sprintf("%s\t%s\t%s\t%s", + entry.Timestamp.Format(time.RFC3339), + entry.Repo, + entry.Path, + entry.Action) +} + +// PrintLogs prints all log entries +func PrintLogs() error { + entries, err := ReadLogs() + if err != nil { + return err + } + + for _, entry := range entries { + fmt.Println(FormatLogEntry(entry)) + } + + return nil +}