diff --git a/src/internal/file_operations_extract.go b/src/internal/file_operations_extract.go index 65fff65c..6c58d7ec 100644 --- a/src/internal/file_operations_extract.go +++ b/src/internal/file_operations_extract.go @@ -6,7 +6,9 @@ import ( "io" "os" "path/filepath" + "runtime" "strings" + "time" "github.com/charmbracelet/bubbles/progress" "github.com/lithammer/shortuuid" @@ -14,6 +16,20 @@ import ( "golift.io/xtractr" ) +func getDefaultFileMode() os.FileMode { + if runtime.GOOS == "windows" { + return 0666 + } + return 0644 +} + +func shouldSkipFile(name string) bool { + // Skip system files across platforms + return strings.HasPrefix(name, "__MACOSX/") || + strings.EqualFold(name, "Thumbs.db") || + strings.EqualFold(name, "desktop.ini") +} + func extractCompressFile(src, dest string) error { id := shortuuid.New() @@ -26,6 +42,7 @@ func extractCompressFile(src, dest string) error { state: inOperation, total: 1, done: 0, + doneTime: time.Time{}, } message := channelMessage{ messageId: id, @@ -33,7 +50,9 @@ func extractCompressFile(src, dest string) error { processNewState: p, } + if len(channel) < 5 { channel <- message + } x := &xtractr.XFile{ FilePath: src, @@ -43,18 +62,24 @@ func extractCompressFile(src, dest string) error { _, _, _, err := xtractr.ExtractFile(x) if err != nil { - p.state = successful + p.state = failure + p.doneTime = time.Now() message.processNewState = p + if len(channel) < 5 { channel <- message + } + outPutLog(fmt.Sprintf("Error extracting %s: %v", src, err)) return err } p.state = successful p.done = 1 - + p.doneTime = time.Now() message.processNewState = p + if len(channel) < 5 { channel <- message - + } + return nil } @@ -63,13 +88,14 @@ func unzip(src, dest string) error { id := shortuuid.New() r, err := zip.OpenReader(src) if err != nil { - return err + return fmt.Errorf("failed to open zip: %w", err) } defer func() { if err := r.Close(); err != nil { - panic(err) + outPutLog(fmt.Sprintf("Error closing zip reader: %v", err)) } }() + totalFiles := len(r.File) // progressbar prog := progress.New(generateGradientColor()) @@ -81,6 +107,7 @@ func unzip(src, dest string) error { state: inOperation, total: totalFiles, done: 0, + doneTime: time.Time{}, } message := channelMessage{ @@ -94,68 +121,96 @@ func unzip(src, dest string) error { rc, err := f.Open() if err != nil { - return err + return fmt.Errorf("failed to open file in zip: %w", err) } defer func() { if err := rc.Close(); err != nil { - panic(err) + outPutLog(fmt.Sprintf("Error closing file reader: %v", err)) } }() path := filepath.Join(dest, f.Name) - // Check for ZipSlip (Directory traversal) - if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) { + // Cross-platform path security check + if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dest)+string(os.PathSeparator)) { return fmt.Errorf("illegal file path: %s", path) } + fileMode := f.Mode() if f.FileInfo().IsDir() { - os.MkdirAll(path, f.Mode()) - } else { - os.MkdirAll(filepath.Dir(path), f.Mode()) - f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + err := os.MkdirAll(path, fileMode) if err != nil { - return fmt.Errorf("error open file: %s", err) + return fmt.Errorf("failed to create directory: %w", err) } - defer func() { - if err := f.Close(); err != nil { - panic(err) - } - }() + return nil + } - _, err = io.Copy(f, rc) + // Create directory structure + if err := os.MkdirAll(filepath.Dir(path), fileMode); err != nil { + return fmt.Errorf("failed to create parent directory: %w", err) + } + // Try default permissions first + outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, getDefaultFileMode()) + if err != nil { + // Fall back to original file permissions + outFile, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { - return fmt.Errorf("error copy file: %s", err) + return fmt.Errorf("failed to create file: %w", err) + } + } + defer func() { + if err := outFile.Close(); err != nil { + outPutLog(fmt.Sprintf("Error closing output file %s: %v", path, err)) } + }() + + if _, err := io.Copy(outFile, rc); err != nil { + return fmt.Errorf("failed to write file content: %w", err) } + return nil } for _, f := range r.File { p.name = icon.ExtractFile + icon.Space + f.Name - if len(channel) < 3 { + if len(channel) < 5 { message.processNewState = p channel <- message } + + if shouldSkipFile(f.Name) { + p.done++ + continue + } + err := extractAndWriteFile(f) if err != nil { p.state = failure message.processNewState = p channel <- message - return err + outPutLog(fmt.Sprintf("Error extracting %s: %v", f.Name, err)) + p.done++ + continue } p.done++ - if len(channel) < 3 { + if len(channel) < 5 { message.processNewState = p - channel <- message + channel <- message } } p.total = totalFiles - p.state = successful + p.doneTime = time.Now() + if p.done == totalFiles { + p.state = successful + } else { + p.state = failure + } message.processNewState = p + if len(channel) < 5 { channel <- message + } return nil } \ No newline at end of file