From 9424c8aa50f28c07dc5f11a515b71288b12ba3d0 Mon Sep 17 00:00:00 2001 From: bereck-work Date: Sat, 22 Nov 2025 08:27:42 +0000 Subject: [PATCH] (Feat): Added a new archive type: RAR --- cmd/zipprine/main.go | 14 ++++ internal/archiver/archiver.go | 4 + internal/archiver/detect.go | 9 +++ internal/archiver/rar.go | 136 ++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 internal/archiver/rar.go diff --git a/cmd/zipprine/main.go b/cmd/zipprine/main.go index ee103c0..5d23254 100644 --- a/cmd/zipprine/main.go +++ b/cmd/zipprine/main.go @@ -4,13 +4,21 @@ import ( "fmt" "os" + "zipprine/internal/cli" "zipprine/internal/ui" + "zipprine/internal/version" "github.com/charmbracelet/huh" ) func main() { + // Try CLI mode first + if cli.Run() { + return + } + fmt.Println(ui.TitleStyle.Render("Zipprine - TUI Archiver")) + fmt.Println(ui.InfoStyle.Render("Version: " + version.Version())) fmt.Println() var operation string @@ -23,6 +31,7 @@ func main() { huh.NewOption("📦 Compress files/folders", "compress"), huh.NewOption("📂 Extract archive", "extract"), huh.NewOption("🔍 Analyze archive", "analyze"), + huh.NewOption("🌐 Fetch from URL", "remote-fetch"), huh.NewOption("📚 Batch compress", "batch-compress"), huh.NewOption("📂 Batch extract", "batch-extract"), huh.NewOption("🔄 Convert archive format", "convert"), @@ -54,6 +63,11 @@ func main() { fmt.Println(ui.ErrorStyle.Render("❌ Error: " + err.Error())) os.Exit(1) } + case "remote-fetch": + if err := ui.RunRemoteFetchFlow(); err != nil { + fmt.Println(ui.ErrorStyle.Render("❌ Error: " + err.Error())) + os.Exit(1) + } case "batch-compress": if err := ui.RunBatchCompressFlow(); err != nil { fmt.Println(ui.ErrorStyle.Render("❌ Error: " + err.Error())) diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 2a2f343..563ae41 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -14,6 +14,8 @@ func Compress(config *models.CompressConfig) error { return createTar(config) case models.GZIP: return createGzip(config) + case models.RAR: + return createRar(config) default: return nil } @@ -29,6 +31,8 @@ func Extract(config *models.ExtractConfig) error { return extractTar(config) case models.GZIP: return extractGzip(config) + case models.RAR: + return extractRar(config) default: return nil } diff --git a/internal/archiver/detect.go b/internal/archiver/detect.go index 907a0b3..113b485 100644 --- a/internal/archiver/detect.go +++ b/internal/archiver/detect.go @@ -26,6 +26,8 @@ func DetectArchiveType(path string) (models.ArchiveType, error) { return models.TAR, nil case ".tgz": return models.TARGZ, nil + case ".rar": + return models.RAR, nil } // Try by magic bytes @@ -70,6 +72,11 @@ func DetectArchiveType(path string) (models.ArchiveType, error) { return models.TAR, nil } + // RAR magic: Rar! (0x52 0x61 0x72 0x21) + if len(header) >= 4 && header[0] == 0x52 && header[1] == 0x61 && header[2] == 0x72 && header[3] == 0x21 { + return models.RAR, nil + } + return models.AUTO, nil } @@ -86,6 +93,8 @@ func Analyze(path string) (*models.ArchiveInfo, error) { return analyzeTar(path, true) case models.TAR: return analyzeTar(path, false) + case models.RAR: + return analyzeRar(path) case models.GZIP: // For GZIP, provide basic file info file, err := os.Open(path) diff --git a/internal/archiver/rar.go b/internal/archiver/rar.go new file mode 100644 index 0000000..41bf84a --- /dev/null +++ b/internal/archiver/rar.go @@ -0,0 +1,136 @@ +package archiver + +import ( + "fmt" + "os" + "path/filepath" + + "zipprine/internal/models" + + "github.com/nwaples/rardecode" +) + +// extractRar extracts a RAR archive +func extractRar(config *models.ExtractConfig) error { + file, err := os.Open(config.ArchivePath) + if err != nil { + return fmt.Errorf("failed to open RAR file: %w", err) + } + defer file.Close() + + reader, err := rardecode.NewReader(file, "") + if err != nil { + return fmt.Errorf("failed to create RAR reader: %w", err) + } + + if err := os.MkdirAll(config.DestPath, 0755); err != nil { + return fmt.Errorf("failed to create destination directory: %w", err) + } + + for { + header, err := reader.Next() + if err != nil { + if err.Error() == "EOF" { + break + } + return fmt.Errorf("failed to read RAR entry: %w", err) + } + + if header.IsDir { + targetPath := filepath.Join(config.DestPath, header.Name) + if err := os.MkdirAll(targetPath, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + continue + } + + targetPath := filepath.Join(config.DestPath, header.Name) + + if _, err := os.Stat(targetPath); err == nil && !config.OverwriteAll { + fmt.Printf("Skipping existing file: %s\n", header.Name) + continue + } + + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + return fmt.Errorf("failed to create parent directory: %w", err) + } + + outFile, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", header.Name, err) + } + + if _, err := outFile.ReadFrom(reader); err != nil { + outFile.Close() + return fmt.Errorf("failed to write file %s: %w", header.Name, err) + } + outFile.Close() + + // Set permissions if requested + if config.PreservePerms { + if err := os.Chmod(targetPath, header.Mode()); err != nil { + fmt.Printf("Warning: failed to set permissions for %s: %v\n", header.Name, err) + } + } + + fmt.Printf("Extracted: %s\n", header.Name) + } + + return nil +} + +// analyzeRar analyzes a RAR archive and returns information about it +func analyzeRar(path string) (*models.ArchiveInfo, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open RAR file: %w", err) + } + defer file.Close() + + fileStat, _ := file.Stat() + + reader, err := rardecode.NewReader(file, "") + if err != nil { + return nil, fmt.Errorf("failed to create RAR reader: %w", err) + } + + info := &models.ArchiveInfo{ + Type: models.RAR, + CompressedSize: fileStat.Size(), + Files: []models.FileInfo{}, + } + + for { + header, err := reader.Next() + if err != nil { + if err.Error() == "EOF" { + break + } + return nil, fmt.Errorf("failed to read RAR entry: %w", err) + } + + if !header.IsDir { + info.FileCount++ + info.TotalSize += header.UnPackedSize + info.Files = append(info.Files, models.FileInfo{ + Name: header.Name, + Size: header.UnPackedSize, + IsDir: header.IsDir, + ModTime: header.ModificationTime.Format("2006-01-02 15:04:05"), + }) + } + } + + if info.TotalSize > 0 { + info.CompressionRatio = float64(info.CompressedSize) / float64(info.TotalSize) + } + + return info, nil +} + +// Note: RAR compression is proprietary and requires a license. +// This implementation only supports extraction using the rardecode library. +// For compression, users should use WinRAR or other licensed tools. +func createRar(config *models.CompressConfig) error { + return fmt.Errorf("RAR compression is not supported due to proprietary format. Please use ZIP, TAR, or TAR.GZ for compression") +}