Introduces core functionality for the Agate library, including snapshot creation, restoration, listing, and deletion. Adds examples for basic usage, gRPC proto definitions, and build/configuration files such as `go.mod` and `Makefile`. The implementation establishes the framework for store integration and placeholder server functionality.
156 lines
4.9 KiB
Go
156 lines
4.9 KiB
Go
package archive
|
||
|
||
import (
|
||
"archive/zip"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
type ArchiveEntryInfo struct {
|
||
Path string // Относительный путь внутри архива
|
||
Size uint64 // Размер файла в байтах (0 для директорий)
|
||
IsDir bool // Является ли запись директорией
|
||
}
|
||
|
||
var ErrFileNotFoundInArchive = errors.New("file not found in archive")
|
||
|
||
// CreateArchive создает ZIP-архив по пути targetPath из содержимого sourceDir.
|
||
func CreateArchive(sourceDir, targetPath string) error {
|
||
info, err := os.Stat(sourceDir)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to stat source directory %s: %w", sourceDir, err)
|
||
}
|
||
if !info.IsDir() {
|
||
return fmt.Errorf("source %s is not a directory", sourceDir)
|
||
}
|
||
|
||
// Создаем файл для ZIP-архива
|
||
outFile, err := os.Create(targetPath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create target archive file %s: %w", targetPath, err)
|
||
}
|
||
defer outFile.Close()
|
||
|
||
// Создаем zip.Writer
|
||
zipWriter := zip.NewWriter(outFile)
|
||
defer zipWriter.Close()
|
||
|
||
// Рекурсивно проходим по директории sourceDir
|
||
err = filepath.Walk(sourceDir, func(filePath string, fileInfo os.FileInfo, walkErr error) error {
|
||
if walkErr != nil {
|
||
return fmt.Errorf("error walking path %s: %w", filePath, walkErr)
|
||
}
|
||
|
||
// Пропускаем саму директорию sourceDir
|
||
if filePath == sourceDir {
|
||
return nil
|
||
}
|
||
|
||
// Создаем относительный путь для записи в архиве
|
||
relativePath := strings.TrimPrefix(filePath, sourceDir+string(filepath.Separator))
|
||
relativePath = filepath.ToSlash(relativePath)
|
||
|
||
// Проверяем, является ли текущий элемент директорией
|
||
if fileInfo.IsDir() {
|
||
// Для директорий добавляем "/" в конец
|
||
_, err = zipWriter.Create(relativePath + "/")
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create directory entry %s in archive: %w", relativePath, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Открываем файл для чтения
|
||
var fileToArchive *os.File
|
||
fileToArchive, err = os.Open(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to open file %s for archiving: %w", filePath, err)
|
||
}
|
||
defer fileToArchive.Close()
|
||
|
||
// Создаем запись в архиве
|
||
var zipEntryWriter io.Writer
|
||
zipEntryWriter, err = zipWriter.Create(relativePath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create entry %s in archive: %w", relativePath, err)
|
||
}
|
||
|
||
// Копируем содержимое файла в запись архива
|
||
if _, err = io.Copy(zipEntryWriter, fileToArchive); err != nil {
|
||
return fmt.Errorf("failed to copy file content %s to archive: %w", filePath, err)
|
||
}
|
||
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
os.Remove(targetPath)
|
||
return fmt.Errorf("failed during directory walk for archiving %s: %w", sourceDir, err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ListArchiveContents читает ZIP-архив и возвращает информацию о его содержимом.
|
||
func ListArchiveContents(archivePath string) ([]ArchiveEntryInfo, error) {
|
||
// Открываем ZIP-архив
|
||
zipReader, err := zip.OpenReader(archivePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to open archive file %s: %w", archivePath, err)
|
||
}
|
||
defer zipReader.Close()
|
||
|
||
var entries []ArchiveEntryInfo
|
||
|
||
// Проходим по всем файлам и директориям внутри архива
|
||
for _, file := range zipReader.File {
|
||
entry := ArchiveEntryInfo{
|
||
Path: file.Name,
|
||
Size: file.UncompressedSize64,
|
||
IsDir: file.FileInfo().IsDir(),
|
||
}
|
||
entries = append(entries, entry)
|
||
}
|
||
|
||
return entries, nil
|
||
}
|
||
|
||
// ExtractFileFromArchive извлекает один файл из ZIP-архива.
|
||
func ExtractFileFromArchive(archivePath, filePathInArchive string, writer io.Writer) error {
|
||
// Открываем ZIP-архив
|
||
zipReader, err := zip.OpenReader(archivePath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to open archive file %s: %w", archivePath, err)
|
||
}
|
||
defer zipReader.Close()
|
||
|
||
// Ищем файл в архиве
|
||
for _, file := range zipReader.File {
|
||
if filepath.ToSlash(file.Name) == filepath.ToSlash(filePathInArchive) {
|
||
// Проверяем, что это не директория
|
||
if file.FileInfo().IsDir() {
|
||
return fmt.Errorf("entry %s in archive is a directory", filePathInArchive)
|
||
}
|
||
|
||
// Открываем файл для чтения
|
||
readCloser, err := file.Open()
|
||
if err != nil {
|
||
return fmt.Errorf("failed to open file %s in archive: %w", filePathInArchive, err)
|
||
}
|
||
defer readCloser.Close()
|
||
|
||
// Копируем содержимое файла в writer
|
||
if _, err := io.Copy(writer, readCloser); err != nil {
|
||
return fmt.Errorf("failed to copy content of %s from archive: %w", filePathInArchive, err)
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
|
||
return ErrFileNotFoundInArchive
|
||
}
|