Add initial implementation of Agate snapshot library
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.
This commit is contained in:
155
archive/archive.go
Normal file
155
archive/archive.go
Normal file
@ -0,0 +1,155 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user