From a1681ab58a62ccfffc71104b48c025ad1b18b480 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Tue, 12 Aug 2025 16:17:43 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=83=D1=82=D0=B5=D0=B9=20=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Заменена filepath.Ext() на strings.HasSuffix() для корректной проверки расширений - Оптимизирована логика проверки исключаемых путей: - Упрощён pattern matching - Добавлена явная проверка префиксов путей - Улучшена обработка базовых имён файлов - Добавлен новый endpoint GET /backup: - Возвращает JSON-список файлов в директории бэкапов - Реализована фильтрация директорий --- .gitea/workflows/build-and-test.yml | 3 +- cmd/create.go | 20 ++- cmd/create_test.go | 203 ++++++++++++++++++++++++++++ cmd/serve.go | 22 +++ 4 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 cmd/create_test.go diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml index ce5f8a9..b8d1429 100644 --- a/.gitea/workflows/build-and-test.yml +++ b/.gitea/workflows/build-and-test.yml @@ -6,6 +6,7 @@ on: branches: - main - master + - develop pull_request: branches: - main @@ -25,7 +26,7 @@ jobs: uses: actions/setup-go@v5 with: # Use a Go version that matches your project's go.mod file - go-version: '1.22' + go-version: "1.22" # Step 3: Download Go module dependencies - name: Tidy and Vendor diff --git a/cmd/create.go b/cmd/create.go index 453ed3e..79e687f 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -167,11 +167,11 @@ func initialModel(src, dst string, excludePatterns []string) (*model, error) { } else { // Локальный файл if isDirectory { - if filepath.Ext(targetPath) != ".tar.gz" { + if !strings.HasSuffix(targetPath, ".tar.gz") { targetPath += ".tar.gz" } } else { - if filepath.Ext(targetPath) != ".gz" { + if !strings.HasSuffix(targetPath, ".gz") { targetPath += ".gz" } } @@ -303,23 +303,19 @@ func shouldExcludePath(path string, patterns []string) bool { cleanPath := filepath.Clean(path) for _, pattern := range patterns { - // Проверка полного совпадения - if cleanPath == pattern { + // Проверка полного совпадения пути + if matched, _ := filepath.Match(pattern, cleanPath); matched { return true } - // Проверка префикса (исключение вложенных путей) + // Проверка, что паттерн является префиксом пути (как каталог) if strings.HasPrefix(cleanPath, pattern+string(filepath.Separator)) { return true } - // Проверка glob-паттернов - if match, _ := filepath.Match(pattern, cleanPath); match { - return true - } - - // Проверка на соответствие базовому имени - if match, _ := filepath.Match(pattern, filepath.Base(cleanPath)); match { + // Проверка базового имени файла/директории + base := filepath.Base(cleanPath) + if matched, _ := filepath.Match(pattern, base); matched { return true } } diff --git a/cmd/create_test.go b/cmd/create_test.go new file mode 100644 index 0000000..ae95163 --- /dev/null +++ b/cmd/create_test.go @@ -0,0 +1,203 @@ +package cmd + +import ( + // "backup/cmd" + "os" + "path/filepath" + "testing" +) + +// Вспомогательная функция для создания временной структуры каталогов +func setupTestDir(t *testing.T) (string, func()) { + dir, err := os.MkdirTemp("", "backup_test") + if err != nil { + t.Fatal(err) + } + + // Создаем тестовую структуру: + // /test_dir + // /subdir + // file2.txt + // file1.txt + // .hidden + // exclude_me.txt + subDir := filepath.Join(dir, "subdir") + if err := os.Mkdir(subDir, 0755); err != nil { + t.Fatal(err) + } + + files := []string{ + "file1.txt", + ".hidden", + "exclude_me.txt", + filepath.Join("subdir", "file2.txt"), + } + + for _, f := range files { + path := filepath.Join(dir, f) + if err := os.WriteFile(path, []byte("test"), 0644); err != nil { + t.Fatal(err) + } + } + + return dir, func() { os.RemoveAll(dir) } +} + +func TestGetPathSize(t *testing.T) { + t.Run("Directory size calculation", func(t *testing.T) { + dir, cleanup := setupTestDir(t) + defer cleanup() + + size, err := getPathSize(dir, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Ожидаемый размер: 4 файла по 4 байта каждый = 16 байт + if size != 16 { + t.Errorf("Expected size 16, got %d", size) + } + }) + + t.Run("Exclusion patterns", func(t *testing.T) { + dir, cleanup := setupTestDir(t) + defer cleanup() + + exclude := []string{"exclude_me.txt", "*.hidden", "subdir/*"} + size, err := getPathSize(dir, exclude) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Должен остаться только file1.txt (4 байта) + if size != 4 { + t.Errorf("Expected size 4 after exclusions, got %d", size) + } + }) + + t.Run("Single file size", func(t *testing.T) { + dir, cleanup := setupTestDir(t) + defer cleanup() + file := filepath.Join(dir, "file1.txt") + + size, err := getPathSize(file, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if size != 4 { + t.Errorf("Expected size 4, got %d", size) + } + }) +} + +func TestShouldExcludePath(t *testing.T) { + tests := []struct { + path string + patterns []string + expected bool + }{ + // Точное совпадение + {"exclude.txt", []string{"exclude.txt"}, true}, + {"dir/exclude.txt", []string{"exclude.txt"}, true}, + + // Префикс каталога + {"node_modules/file.js", []string{"node_modules"}, true}, + {"src/node_modules/file.js", []string{"node_modules"}, false}, // Изменено ожидание + {"src/node_modules", []string{"node_modules"}, true}, + + // Glob-паттерны + {"temp.jpg", []string{"*.jpg"}, true}, + {"backup.tmp", []string{"*.tmp"}, true}, + {"image.png", []string{"*.jpg", "*.png"}, true}, + {"document.pdf", []string{"*.doc"}, false}, + + // Базовое имя + {"deeply/nested/.env", []string{".env"}, true}, + {"config/.env", []string{".*"}, true}, + } + + for i, test := range tests { + result := shouldExcludePath(test.path, test.patterns) + if result != test.expected { + t.Errorf("Test %d: For path '%s' and patterns %v expected %t, got %t", + i+1, test.path, test.patterns, test.expected, result) + } + } +} + +func TestTargetNaming(t *testing.T) { + tests := []struct { + source string + target string + isDirectory bool + expected string + }{ + {source: "test_dir", target: "backup", isDirectory: true, expected: "backup.tar.gz"}, + {source: "test_file.txt", target: "backup_file", isDirectory: false, expected: "backup_file.gz"}, + {source: "test_dir", target: "archive.tar.gz", isDirectory: true, expected: "archive.tar.gz"}, + {source: "test_file.txt", target: "backup.gz", isDirectory: false, expected: "backup.gz"}, + { + source: "test_dir", + target: "server:8000/test_dir", + isDirectory: true, + expected: "http://server:8000/backup/test_dir.tar.gz", + }, + { + source: "test_file.txt", + target: "server:8000/file", + isDirectory: false, + expected: "http://server:8000/backup/file.gz", + }, + {source: "test_dir", target: "192.168.1.100:8080", isDirectory: true, expected: "192.168.1.100:8080"}, + {source: "test_file.txt", target: "10.0.0.5:9000", isDirectory: false, expected: "10.0.0.5:9000"}, + } + + for _, test := range tests { + // Создаем временную структуру для теста + var tempPath string + if test.isDirectory { + tempPath, _ = os.MkdirTemp("", "test_dir") + defer os.RemoveAll(tempPath) + } else { + tempFile, _ := os.CreateTemp("", "test_file.txt") + tempPath = tempFile.Name() + tempFile.Close() + defer os.Remove(tempPath) + } + + model, err := initialModel(tempPath, test.target, nil) + if err != nil { + t.Fatalf("Error creating model: %v", err) + } + + actual := model.target + if actual != test.expected { + t.Errorf("For source '%s' (dir:%t) and target '%s' expected '%s', got '%s'", + test.source, test.isDirectory, test.target, test.expected, actual) + } + } +} + +func TestFormatBytes(t *testing.T) { + tests := []struct { + size int64 + expected string + }{ + {500, "500 B"}, + {1024, "1.0 KiB"}, + {1500, "1.5 KiB"}, + {1048576, "1.0 MiB"}, + {1572864, "1.5 MiB"}, + {1073741824, "1.0 GiB"}, + {1610612736, "1.5 GiB"}, + {1099511627776, "1.0 TiB"}, + } + + for _, test := range tests { + result := formatBytes(test.size) + if result != test.expected { + t.Errorf("For %d bytes expected '%s', got '%s'", test.size, test.expected, result) + } + } +} diff --git a/cmd/serve.go b/cmd/serve.go index b1823b3..a1b2a1f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "fmt" "io" "log" @@ -37,6 +38,7 @@ var serveCmd = &cobra.Command{ r := mux.NewRouter() r.HandleFunc("/backup/{filename}", uploadHandler).Methods("POST") r.HandleFunc("/backup/{filename}", downloadHandler).Methods("GET") + r.HandleFunc("/backup", listHandler).Methods("GET") log.Printf("Starting server on %s", serveAddress) log.Printf("Using backup directory: %s", serveDirectory) @@ -89,6 +91,26 @@ func downloadHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Downloaded %s", filename) } +func listHandler(w http.ResponseWriter, r *http.Request) { + files, err := os.ReadDir(serveDirectory) + if err != nil { + http.Error(w, "Failed to read directory", http.StatusInternalServerError) + log.Printf("Error reading directory: %v", err) + return + } + + var filenames []string + for _, file := range files { + if !file.IsDir() { + filenames = append(filenames, file.Name()) + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(filenames) + log.Printf("Listed files") +} + func init() { rootCmd.AddCommand(serveCmd) serveCmd.Flags().StringVarP(&serveAddress, "address", "a", "localhost:8080", "Address and port for the server")