(Feat): Added a new archive type: RAR
This commit is contained in:
@@ -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()))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
136
internal/archiver/rar.go
Normal 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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user