(Feat): Added a new archive type: RAR

This commit is contained in:
2025-11-22 08:27:42 +00:00
parent 6f299ce541
commit 9424c8aa50
4 changed files with 163 additions and 0 deletions

View File

@@ -4,13 +4,21 @@ import (
"fmt" "fmt"
"os" "os"
"zipprine/internal/cli"
"zipprine/internal/ui" "zipprine/internal/ui"
"zipprine/internal/version"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
) )
func main() { func main() {
// Try CLI mode first
if cli.Run() {
return
}
fmt.Println(ui.TitleStyle.Render("Zipprine - TUI Archiver")) fmt.Println(ui.TitleStyle.Render("Zipprine - TUI Archiver"))
fmt.Println(ui.InfoStyle.Render("Version: " + version.Version()))
fmt.Println() fmt.Println()
var operation string var operation string
@@ -23,6 +31,7 @@ func main() {
huh.NewOption("📦 Compress files/folders", "compress"), huh.NewOption("📦 Compress files/folders", "compress"),
huh.NewOption("📂 Extract archive", "extract"), huh.NewOption("📂 Extract archive", "extract"),
huh.NewOption("🔍 Analyze archive", "analyze"), huh.NewOption("🔍 Analyze archive", "analyze"),
huh.NewOption("🌐 Fetch from URL", "remote-fetch"),
huh.NewOption("📚 Batch compress", "batch-compress"), huh.NewOption("📚 Batch compress", "batch-compress"),
huh.NewOption("📂 Batch extract", "batch-extract"), huh.NewOption("📂 Batch extract", "batch-extract"),
huh.NewOption("🔄 Convert archive format", "convert"), huh.NewOption("🔄 Convert archive format", "convert"),
@@ -54,6 +63,11 @@ func main() {
fmt.Println(ui.ErrorStyle.Render("❌ Error: " + err.Error())) fmt.Println(ui.ErrorStyle.Render("❌ Error: " + err.Error()))
os.Exit(1) 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": case "batch-compress":
if err := ui.RunBatchCompressFlow(); err != nil { if err := ui.RunBatchCompressFlow(); err != nil {
fmt.Println(ui.ErrorStyle.Render("❌ Error: " + err.Error())) fmt.Println(ui.ErrorStyle.Render("❌ Error: " + err.Error()))

View File

@@ -14,6 +14,8 @@ func Compress(config *models.CompressConfig) error {
return createTar(config) return createTar(config)
case models.GZIP: case models.GZIP:
return createGzip(config) return createGzip(config)
case models.RAR:
return createRar(config)
default: default:
return nil return nil
} }
@@ -29,6 +31,8 @@ func Extract(config *models.ExtractConfig) error {
return extractTar(config) return extractTar(config)
case models.GZIP: case models.GZIP:
return extractGzip(config) return extractGzip(config)
case models.RAR:
return extractRar(config)
default: default:
return nil return nil
} }

View File

@@ -26,6 +26,8 @@ func DetectArchiveType(path string) (models.ArchiveType, error) {
return models.TAR, nil return models.TAR, nil
case ".tgz": case ".tgz":
return models.TARGZ, nil return models.TARGZ, nil
case ".rar":
return models.RAR, nil
} }
// Try by magic bytes // Try by magic bytes
@@ -70,6 +72,11 @@ func DetectArchiveType(path string) (models.ArchiveType, error) {
return models.TAR, nil 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 return models.AUTO, nil
} }
@@ -86,6 +93,8 @@ func Analyze(path string) (*models.ArchiveInfo, error) {
return analyzeTar(path, true) return analyzeTar(path, true)
case models.TAR: case models.TAR:
return analyzeTar(path, false) return analyzeTar(path, false)
case models.RAR:
return analyzeRar(path)
case models.GZIP: case models.GZIP:
// For GZIP, provide basic file info // For GZIP, provide basic file info
file, err := os.Open(path) file, err := os.Open(path)

136
internal/archiver/rar.go Normal file
View File

@@ -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")
}