Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/internal/file_operation_compress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package internal

import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestZipSources(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T, tempDir string) ([]string, error)
expectedFiles map[string]string
expectError bool
}{
{
name: "multiple directories with subdirectories",
setupFunc: func(t *testing.T, tempDir string) ([]string, error) {
testDir1 := filepath.Join(tempDir, "testdir1")
testDir2 := filepath.Join(tempDir, "testdir2")
subDir := filepath.Join(testDir1, "subdir")
setupDirectories(t, testDir1, testDir2, subDir)
setupFilesWithData(t, []byte("Content of file1"), filepath.Join(testDir1, "file1.txt"))
setupFilesWithData(t, []byte("Content of file2"), filepath.Join(subDir, "file2.txt"))
setupFilesWithData(t, []byte("Content of file3"), filepath.Join(testDir2, "file3.txt"))

return []string{testDir1, testDir2}, nil
},
expectedFiles: map[string]string{
"testdir1/": "",
"testdir1/file1.txt": "Content of file1",
"testdir1/subdir/": "",
"testdir1/subdir/file2.txt": "Content of file2",
"testdir2/": "",
"testdir2/file3.txt": "Content of file3",
},
expectError: false,
},
{
name: "single file",
setupFunc: func(t *testing.T, tempDir string) ([]string, error) {
testFile := filepath.Join(tempDir, "single.txt")
setupFilesWithData(t, []byte("Single file content"), testFile)
return []string{testFile}, nil
},
expectedFiles: map[string]string{
"single.txt": "Single file content",
},
expectError: false,
},
{
name: "empty list",
setupFunc: func(_ *testing.T, _ string) ([]string, error) {
return []string{}, nil
},
expectedFiles: map[string]string{},
expectError: false,
},
{
name: "non-existent source",
setupFunc: func(_ *testing.T, _ string) ([]string, error) {
return []string{"/non/existent/path"}, nil
},
expectedFiles: nil,
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
sources, err := tt.setupFunc(t, tempDir)
if err != nil {
t.Fatalf("Setup failed: %v", err)
}

targetZip := filepath.Join(tempDir, "test.zip")
err = zipSources(sources, targetZip)

if tt.expectError {
require.Error(t, err, "zipSources should return error")
return
}

require.NoError(t, err, "zipSources should not return error")

zipReader, err := zip.OpenReader(targetZip)
require.NoError(t, err, "should be able to open ZIP file")
defer zipReader.Close()

require.Equal(t, len(tt.expectedFiles), len(zipReader.File), "ZIP should contain expected number of files")

foundFiles := make(map[string]string)
for _, file := range zipReader.File {
foundFiles[file.Name] = ""
if !strings.HasSuffix(file.Name, "/") {
rc, err := file.Open()
require.NoError(t, err, "should be able to open file %s in ZIP", file.Name)

content, err := io.ReadAll(rc)
rc.Close()
require.NoError(t, err, "should be able to read file %s", file.Name)

foundFiles[file.Name] = string(content)
}
}

for expectedFile, expectedContent := range tt.expectedFiles {
foundContent, exists := foundFiles[expectedFile]
require.True(t, exists, "expected file %s should be found in ZIP", expectedFile)
if expectedContent != "" {
require.Equal(t, expectedContent, foundContent, "content should match for file %s", expectedFile)
}
}

for foundFile := range foundFiles {
_, expected := tt.expectedFiles[foundFile]
require.True(t, expected, "unexpected file %s found in ZIP", foundFile)
}
})
}
}

func TestZipSourcesInvalidTarget(t *testing.T) {
tempDir := t.TempDir()
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test"), 0644)
require.NoError(t, err, "should be able to create test file")

invalidTarget := "/invalid/path/test.zip"
err = zipSources([]string{testFile}, invalidTarget)
require.Error(t, err, "zipSources should return error for invalid target")
}
134 changes: 71 additions & 63 deletions src/internal/file_operations_compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,35 @@ package internal

import (
"archive/zip"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"

"github.com/charmbracelet/bubbles/progress"
"github.com/lithammer/shortuuid"
"github.com/yorukot/superfile/src/config/icon"
"github.com/yorukot/superfile/src/internal/common"
)

func zipSource(source, target string) error {
func zipSources(sources []string, target string) error {
id := shortuuid.New()
prog := progress.New()
prog.PercentageStyle = common.FooterStyle
var err error

totalFiles, err := countFiles(source)

if err != nil {
slog.Error("Error while zip file count files ", "error", err)
totalFiles := 0
for _, src := range sources {
if _, err = os.Stat(src); os.IsNotExist(err) {
return fmt.Errorf("source path does not exist: %s", src)
}
count, e := countFiles(src)
if e != nil {
slog.Error("Error while zip file count files ", "error", e)
}
totalFiles += count
}

p := process{
Expand All @@ -31,15 +40,14 @@ func zipSource(source, target string) error {
total: totalFiles,
done: 0,
}

message := channelMessage{
messageID: id,
messageType: sendProcess,
processNewState: p,
}

_, err = os.Stat(target)
if os.IsExist(err) {
if err == nil {
p.name = icon.CompressFile + icon.Space + "File already exist"
message.processNewState = p
channel <- message
Expand All @@ -51,74 +59,74 @@ func zipSource(source, target string) error {
return err
}
defer f.Close()

writer := zip.NewWriter(f)
defer writer.Close()

err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
p.name = icon.CompressFile + icon.Space + filepath.Base(path)
if len(channel) < 5 {
message.processNewState = p
channel <- message
}

if err != nil {
return err
}

header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}

header.Method = zip.Deflate

header.Name, err = filepath.Rel(filepath.Dir(source), path)
if err != nil {
return err
}
if info.IsDir() {
header.Name += "/"
}

headerWriter, err := writer.CreateHeader(header)
if err != nil {
return err
}

if info.IsDir() {
for _, src := range sources {
srcParentDir := filepath.Dir(src)
err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
p.name = icon.CompressFile + icon.Space + filepath.Base(path)
if len(channel) < 5 {
message.processNewState = p
channel <- message
}
if err != nil {
return err
}
relPath, err := filepath.Rel(srcParentDir, path)
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Method = zip.Deflate
header.Name = relPath
if info.IsDir() {
header.Name += "/"
}
headerWriter, err := writer.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(headerWriter, file)
if err != nil {
return err
}
p.done++
if len(channel) < 5 {
message.processNewState = p
channel <- message
}
return nil
}

f, err := os.Open(path)
})
if err != nil {
return err
}
defer f.Close()

_, err = io.Copy(headerWriter, f)
if err != nil {
return err
}
p.done++
if len(channel) < 5 {
slog.Error("Error while zip file", "error", err)
p.state = failure
message.processNewState = p
channel <- message
return err
}
return nil
})

if err != nil {
slog.Error("Error while zip file", "error", err)
p.state = failure
message.processNewState = p
channel <- message
}

p.state = successful
p.done = totalFiles

message.processNewState = p
channel <- message

return nil
}

func getZipArchiveName(base string) (string, error) {
zipName := strings.TrimSuffix(base, filepath.Ext(base)) + ".zip"
zipName, err := renameIfDuplicate(zipName)
return zipName, err
}
Loading
Loading