package config import ( "bufio" "fmt" "io" "os" "path/filepath" "strings" "time" "git.gostacks.org/iwasforcedtobehere/fastestmirror/internal/distro" ) // ConfigManager handles backup and modification of distribution config files type ConfigManager struct { DistroInfo *distro.DistroInfo BackupDir string } // NewConfigManager creates a new configuration manager func NewConfigManager(distroInfo *distro.DistroInfo) *ConfigManager { return &ConfigManager{ DistroInfo: distroInfo, BackupDir: "/etc/fastestmirror/backups", } } // BackupConfig creates a backup of the current configuration func (cm *ConfigManager) BackupConfig() (string, error) { configFile := cm.DistroInfo.GetConfigFile() if configFile == "" { return "", fmt.Errorf("no configuration file defined for %s", cm.DistroInfo.Family) } // Create backup directory if it doesn't exist if err := os.MkdirAll(cm.BackupDir, 0755); err != nil { return "", fmt.Errorf("failed to create backup directory: %w", err) } // Generate backup filename with timestamp timestamp := time.Now().Format("20060102-150405") backupName := fmt.Sprintf("%s.backup.%s", filepath.Base(configFile), timestamp) backupPath := filepath.Join(cm.BackupDir, backupName) // Copy original file to backup location if err := cm.copyFile(configFile, backupPath); err != nil { return "", fmt.Errorf("failed to create backup: %w", err) } return backupPath, nil } // ApplyMirror applies the fastest mirror to the configuration func (cm *ConfigManager) ApplyMirror(mirrorURL string) error { switch cm.DistroInfo.Family { case "debian": return cm.applyDebianMirror(mirrorURL) case "arch": return cm.applyArchMirror(mirrorURL) default: return fmt.Errorf("configuration management for %s family not implemented yet", cm.DistroInfo.Family) } } // applyDebianMirror updates /etc/apt/sources.list for Debian-based systems func (cm *ConfigManager) applyDebianMirror(mirrorURL string) error { configFile := "/etc/apt/sources.list" // Read current configuration file, err := os.Open(configFile) if err != nil { return fmt.Errorf("failed to read %s: %w", configFile, err) } defer file.Close() var lines []string scanner := bufio.NewScanner(file) // Process each line and replace mirror URLs for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") { // Keep comments and empty lines as-is lines = append(lines, line) continue } // Parse and update deb/deb-src lines if strings.HasPrefix(strings.TrimSpace(line), "deb") { updatedLine := cm.updateDebianLine(line, mirrorURL) lines = append(lines, updatedLine) } else { lines = append(lines, line) } } if err := scanner.Err(); err != nil { return fmt.Errorf("error reading %s: %w", configFile, err) } // Write updated configuration return cm.writeLines(configFile, lines) } // updateDebianLine updates a single deb line with the new mirror URL func (cm *ConfigManager) updateDebianLine(line, newMirrorURL string) string { parts := strings.Fields(line) if len(parts) < 3 { return line // Invalid line, keep as-is } // parts[0] = "deb" or "deb-src" // parts[1] = URL // parts[2] = suite/codename // parts[3+] = components parts[1] = strings.TrimSuffix(newMirrorURL, "/") + "/" return strings.Join(parts, " ") } // applyArchMirror updates /etc/pacman.d/mirrorlist for Arch-based systems func (cm *ConfigManager) applyArchMirror(mirrorURL string) error { configFile := "/etc/pacman.d/mirrorlist" // Read current configuration file, err := os.Open(configFile) if err != nil { return fmt.Errorf("failed to read %s: %w", configFile, err) } defer file.Close() var lines []string firstMirrorAdded := false scanner := bufio.NewScanner(file) // Add the fastest mirror at the top (uncommented) lines = append(lines, fmt.Sprintf("# FastestMirror: Added %s", time.Now().Format("2006-01-02 15:04:05"))) lines = append(lines, fmt.Sprintf("Server = %s/$repo/os/$arch", strings.TrimSuffix(mirrorURL, "/"))) lines = append(lines, "") firstMirrorAdded = true // Process existing lines, commenting out other Server entries for scanner.Scan() { line := scanner.Text() trimmed := strings.TrimSpace(line) // Comment out existing Server lines (but keep them for reference) if strings.HasPrefix(trimmed, "Server = ") && firstMirrorAdded { lines = append(lines, "#"+line) } else { lines = append(lines, line) } } if err := scanner.Err(); err != nil { return fmt.Errorf("error reading %s: %w", configFile, err) } // Write updated configuration return cm.writeLines(configFile, lines) } // copyFile copies a file from src to dst func (cm *ConfigManager) copyFile(src, dst string) error { sourceFile, err := os.Open(src) if err != nil { return err } defer sourceFile.Close() destFile, err := os.Create(dst) if err != nil { return err } defer destFile.Close() _, err = io.Copy(destFile, sourceFile) if err != nil { return err } // Copy file permissions srcInfo, err := os.Stat(src) if err != nil { return err } return os.Chmod(dst, srcInfo.Mode()) } // writeLines writes lines to a file func (cm *ConfigManager) writeLines(filename string, lines []string) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() writer := bufio.NewWriter(file) for _, line := range lines { if _, err := writer.WriteString(line + "\n"); err != nil { return err } } return writer.Flush() } // RestoreBackup restores a configuration from backup func (cm *ConfigManager) RestoreBackup(backupPath string) error { configFile := cm.DistroInfo.GetConfigFile() if configFile == "" { return fmt.Errorf("no configuration file defined for %s", cm.DistroInfo.Family) } return cm.copyFile(backupPath, configFile) }