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:
Александр Лазаренко 2025-04-24 02:44:16 +03:00
commit 7e9cea9227
Signed by: Kerblif
GPG Key ID: 5AFAD6640F4670C3
20 changed files with 2649 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
grpc/google
grpc/grafeas
.idea

16
Makefile Normal file
View File

@ -0,0 +1,16 @@
download-third-party:
rm -rf ./grpc/third_party
mkdir -p ./grpc/third_party
cd ./grpc/third_party && git clone https://github.com/googleapis/googleapis.git
mv ./grpc/third_party/googleapis/google ./grpc
mv ./grpc/third_party/googleapis/grafeas ./grpc
rm -rf ./grpc/third_party
gen-proto-geolocation:
mkdir -p ./grpc
@protoc -I ./grpc \
--go_out=grpc --go_opt paths=source_relative \
--go-grpc_out=grpc --go-grpc_opt paths=source_relative \
--grpc-gateway_out=grpc --grpc-gateway_opt paths=source_relative \
./grpc/snapshot.proto

177
api.go Normal file
View File

@ -0,0 +1,177 @@
package agate
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"unprism.ru/KRBL/agate/store"
)
// AgateOptions defines configuration options for the Agate library.
type AgateOptions struct {
// WorkDir is the directory where snapshots will be stored and managed.
WorkDir string
// OpenFunc is called after a snapshot is restored to initialize resources.
// The parameter is the directory where the snapshot was extracted.
OpenFunc func(dir string) error
// CloseFunc is called before a snapshot is created or restored to clean up resources.
CloseFunc func() error
// MetadataStore is the implementation of the metadata store to use.
// Use the stores package to initialize the default implementation:
// metadataStore, err := stores.NewDefaultMetadataStore(metadataDir)
MetadataStore store.MetadataStore
// BlobStore is the implementation of the blob store to use.
// Use the stores package to initialize the default implementation:
// blobStore, err := stores.NewDefaultBlobStore(blobsDir)
BlobStore store.BlobStore
}
// Agate is the main entry point for the snapshot library.
type Agate struct {
manager SnapshotManager
options AgateOptions
metadataDir string
blobsDir string
}
// New initializes a new instance of the Agate library with the given options.
func New(options AgateOptions) (*Agate, error) {
if options.WorkDir == "" {
return nil, errors.New("work directory cannot be empty")
}
// Create the work directory if it doesn't exist
if err := os.MkdirAll(options.WorkDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create work directory: %w", err)
}
// Create subdirectories for metadata and blobs
metadataDir := filepath.Join(options.WorkDir, "metadata")
blobsDir := filepath.Join(options.WorkDir, "blobs")
if err := os.MkdirAll(metadataDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create metadata directory: %w", err)
}
if err := os.MkdirAll(blobsDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create blobs directory: %w", err)
}
var metadataStore store.MetadataStore
var blobStore store.BlobStore
var err error
// Use provided stores or initialize default ones
if options.MetadataStore != nil {
metadataStore = options.MetadataStore
} else {
// For default implementation, the user needs to initialize and provide the stores
return nil, errors.New("metadata store must be provided")
}
if options.BlobStore != nil {
blobStore = options.BlobStore
} else {
// For default implementation, the user needs to initialize and provide the stores
return nil, errors.New("blob store must be provided")
}
// Create the snapshot manager
manager, err := CreateSnapshotManager(metadataStore, blobStore)
if err != nil {
return nil, fmt.Errorf("failed to create snapshot manager: %w", err)
}
return &Agate{
manager: manager,
options: options,
metadataDir: metadataDir,
blobsDir: blobsDir,
}, nil
}
// SaveSnapshot creates a new snapshot from the current state of the work directory.
// If parentID is provided, it will be set as the parent of the new snapshot.
// Returns the ID of the created snapshot.
func (a *Agate) SaveSnapshot(ctx context.Context, name string, parentID string) (string, error) {
// Call CloseFunc if provided
if a.options.CloseFunc != nil {
if err := a.options.CloseFunc(); err != nil {
return "", fmt.Errorf("failed to close resources before snapshot: %w", err)
}
}
// Create the snapshot
snapshot, err := a.manager.CreateSnapshot(ctx, a.options.WorkDir, name, parentID)
if err != nil {
return "", fmt.Errorf("failed to create snapshot: %w", err)
}
// Call OpenFunc if provided
if a.options.OpenFunc != nil {
if err := a.options.OpenFunc(a.options.WorkDir); err != nil {
return "", fmt.Errorf("failed to open resources after snapshot: %w", err)
}
}
return snapshot.ID, nil
}
// RestoreSnapshot extracts a snapshot to the work directory.
func (a *Agate) RestoreSnapshot(ctx context.Context, snapshotID string) error {
// Call CloseFunc if provided
if a.options.CloseFunc != nil {
if err := a.options.CloseFunc(); err != nil {
return fmt.Errorf("failed to close resources before restore: %w", err)
}
}
// Extract the snapshot
if err := a.manager.ExtractSnapshot(ctx, snapshotID, a.options.WorkDir); err != nil {
return fmt.Errorf("failed to extract snapshot: %w", err)
}
// Call OpenFunc if provided
if a.options.OpenFunc != nil {
if err := a.options.OpenFunc(a.options.WorkDir); err != nil {
return fmt.Errorf("failed to open resources after restore: %w", err)
}
}
return nil
}
// ListSnapshots returns a list of all available snapshots.
func (a *Agate) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) {
return a.manager.ListSnapshots(ctx)
}
// GetSnapshotDetails returns detailed information about a specific snapshot.
func (a *Agate) GetSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
return a.manager.GetSnapshotDetails(ctx, snapshotID)
}
// DeleteSnapshot removes a snapshot.
func (a *Agate) DeleteSnapshot(ctx context.Context, snapshotID string) error {
return a.manager.DeleteSnapshot(ctx, snapshotID)
}
// Close releases all resources used by the Agate instance.
func (a *Agate) Close() error {
// Currently, we don't have a way to close the manager directly
// This would be a good addition in the future
return nil
}
// StartServer starts a gRPC server to share snapshots.
// This is a placeholder for future implementation.
func (a *Agate) StartServer(ctx context.Context, address string) error {
return errors.New("server functionality not implemented yet")
}

155
archive/archive.go Normal file
View 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
}

BIN
basic_usage Executable file

Binary file not shown.

45
errors.go Normal file
View File

@ -0,0 +1,45 @@
package agate
import "errors"
// Определяем стандартные ошибки, которые могут возникать при работе со снапшотами.
// Использование стандартных переменных ошибок позволяет легко проверять тип ошибки
// с помощью errors.Is().
var (
// ErrNotFound означает, что снапшот с указанным ID не найден.
ErrNotFound = errors.New("snapshot not found")
// ErrAlreadyExists означает, что снапшот с таким ID или именем уже существует
// (если требуется уникальность имен).
ErrAlreadyExists = errors.New("snapshot already exists")
// ErrInvalidID означает, что предоставленный ID имеет неверный формат (например, не UUID).
ErrInvalidID = errors.New("invalid snapshot ID format")
// ErrParentNotFound означает, что указанный родительский снапшот (ParentID) не найден.
ErrParentNotFound = errors.New("parent snapshot not found")
// ErrSourceNotFound означает, что исходный путь (директория или файл) не найден.
ErrSourceNotFound = errors.New("source path not found")
// ErrSourceNotDirectory означает, что исходный путь не является директорией.
ErrSourceNotDirectory = errors.New("source path is not a directory")
// ErrCreateFailed общая ошибка при создании снапшота.
ErrCreateFailed = errors.New("failed to create snapshot")
// ErrDeleteFailed общая ошибка при удалении снапшота.
ErrDeleteFailed = errors.New("failed to delete snapshot")
// ErrListFailed общая ошибка при получении списка снапшотов.
ErrListFailed = errors.New("failed to list snapshots")
// ErrGetDetailsFailed общая ошибка при получении деталей снапшота.
ErrGetDetailsFailed = errors.New("failed to get snapshot details")
// ErrFileNotFound общая ошибка, если файл не найден (может использоваться внутри OpenFile).
ErrFileNotFound = errors.New("file not found within snapshot")
// ErrOperationFailed общая ошибка для неудачных операций.
ErrOperationFailed = errors.New("snapshot operation failed")
)

121
examples/basic_usage.go Normal file
View File

@ -0,0 +1,121 @@
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"unprism.ru/KRBL/agate"
"unprism.ru/KRBL/agate/stores"
)
func main() {
// Create a temporary directory for our example
workDir, err := os.MkdirTemp("", "agate-example-*")
if err != nil {
log.Fatalf("Failed to create work directory: %v", err)
}
defer os.RemoveAll(workDir) // Clean up when done
// Create directories for metadata and blobs
metadataDir := filepath.Join(workDir, "metadata")
blobsDir := filepath.Join(workDir, "blobs")
if err := os.MkdirAll(metadataDir, 0755); err != nil {
log.Fatalf("Failed to create metadata directory: %v", err)
}
if err := os.MkdirAll(blobsDir, 0755); err != nil {
log.Fatalf("Failed to create blobs directory: %v", err)
}
// Initialize the default stores
metadataStore, blobStore, err := stores.InitDefaultStores(workDir)
if err != nil {
log.Fatalf("Failed to initialize stores: %v", err)
}
defer metadataStore.Close() // Clean up when done
// Create a directory with some data to snapshot
dataDir := filepath.Join(workDir, "data")
if err := os.MkdirAll(dataDir, 0755); err != nil {
log.Fatalf("Failed to create data directory: %v", err)
}
// Create a sample file
sampleFile := filepath.Join(dataDir, "sample.txt")
if err := os.WriteFile(sampleFile, []byte("Hello, Agate!"), 0644); err != nil {
log.Fatalf("Failed to create sample file: %v", err)
}
// Define open and close functions
openFunc := func(dir string) error {
fmt.Printf("Opening resources in directory: %s\n", dir)
return nil
}
closeFunc := func() error {
fmt.Println("Closing resources...")
return nil
}
// Initialize Agate
agateOptions := agate.AgateOptions{
WorkDir: dataDir,
OpenFunc: openFunc,
CloseFunc: closeFunc,
MetadataStore: metadataStore,
BlobStore: blobStore,
}
ag, err := agate.New(agateOptions)
if err != nil {
log.Fatalf("Failed to initialize Agate: %v", err)
}
defer ag.Close()
// Create a snapshot
ctx := context.Background()
snapshotID, err := ag.SaveSnapshot(ctx, "My First Snapshot", "")
if err != nil {
log.Fatalf("Failed to create snapshot: %v", err)
}
fmt.Printf("Created snapshot with ID: %s\n", snapshotID)
// List snapshots
snapshots, err := ag.ListSnapshots(ctx)
if err != nil {
log.Fatalf("Failed to list snapshots: %v", err)
}
fmt.Printf("Found %d snapshots:\n", len(snapshots))
for _, s := range snapshots {
fmt.Printf(" - %s: %s (created at %s)\n", s.ID, s.Name, s.CreationTime.Format("2006-01-02 15:04:05"))
}
// Modify the file
if err := os.WriteFile(sampleFile, []byte("Hello, Agate! (modified)"), 0644); err != nil {
log.Fatalf("Failed to modify sample file: %v", err)
}
// Create another snapshot with the first one as parent
snapshotID2, err := ag.SaveSnapshot(ctx, "My Second Snapshot", snapshotID)
if err != nil {
log.Fatalf("Failed to create second snapshot: %v", err)
}
fmt.Printf("Created second snapshot with ID: %s\n", snapshotID2)
// Restore the first snapshot
if err := ag.RestoreSnapshot(ctx, snapshotID); err != nil {
log.Fatalf("Failed to restore snapshot: %v", err)
}
fmt.Println("Restored first snapshot")
// Read the file content to verify it was restored
content, err := os.ReadFile(sampleFile)
if err != nil {
log.Fatalf("Failed to read sample file: %v", err)
}
fmt.Printf("File content after restore: %s\n", content)
fmt.Println("Example completed successfully!")
}

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
module unprism.ru/KRBL/agate
go 1.24.0
require (
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
github.com/mattn/go-sqlite3 v1.14.28
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f
google.golang.org/grpc v1.72.0
google.golang.org/protobuf v1.36.6
)
require (
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
)

40
go.sum Normal file
View File

@ -0,0 +1,40 @@
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=

545
grpc/snapshot.pb.go Normal file
View File

@ -0,0 +1,545 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v4.25.3
// source: snapshot.proto
package grpc
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Метаданные файла внутри снапшота
type FileInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // Относительный путь файла внутри снапшота
SizeBytes int64 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` // Размер файла в байтах
Sha256Hash string `protobuf:"bytes,3,opt,name=sha256_hash,json=sha256Hash,proto3" json:"sha256_hash,omitempty"` // Хеш-сумма файла (SHA256)
IsDir bool `protobuf:"varint,4,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` // Является ли запись директорией
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FileInfo) Reset() {
*x = FileInfo{}
mi := &file_snapshot_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FileInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FileInfo) ProtoMessage() {}
func (x *FileInfo) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FileInfo.ProtoReflect.Descriptor instead.
func (*FileInfo) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{0}
}
func (x *FileInfo) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *FileInfo) GetSizeBytes() int64 {
if x != nil {
return x.SizeBytes
}
return 0
}
func (x *FileInfo) GetSha256Hash() string {
if x != nil {
return x.Sha256Hash
}
return ""
}
func (x *FileInfo) GetIsDir() bool {
if x != nil {
return x.IsDir
}
return false
}
// Краткая информация о снапшоте
type SnapshotInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // Уникальный ID снапшота (UUID)
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // Имя снапшота
ParentId string `protobuf:"bytes,3,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty"` // ID родительского снапшота (может быть пустым)
CreationTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=creation_time,json=creationTime,proto3" json:"creation_time,omitempty"` // Время создания
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SnapshotInfo) Reset() {
*x = SnapshotInfo{}
mi := &file_snapshot_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SnapshotInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SnapshotInfo) ProtoMessage() {}
func (x *SnapshotInfo) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SnapshotInfo.ProtoReflect.Descriptor instead.
func (*SnapshotInfo) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{1}
}
func (x *SnapshotInfo) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *SnapshotInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *SnapshotInfo) GetParentId() string {
if x != nil {
return x.ParentId
}
return ""
}
func (x *SnapshotInfo) GetCreationTime() *timestamppb.Timestamp {
if x != nil {
return x.CreationTime
}
return nil
}
// Детальная информация о снапшоте
type SnapshotDetails struct {
state protoimpl.MessageState `protogen:"open.v1"`
Info *SnapshotInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` // Краткая информация
Files []*FileInfo `protobuf:"bytes,2,rep,name=files,proto3" json:"files,omitempty"` // Список файлов в снапшоте
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SnapshotDetails) Reset() {
*x = SnapshotDetails{}
mi := &file_snapshot_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SnapshotDetails) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SnapshotDetails) ProtoMessage() {}
func (x *SnapshotDetails) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SnapshotDetails.ProtoReflect.Descriptor instead.
func (*SnapshotDetails) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{2}
}
func (x *SnapshotDetails) GetInfo() *SnapshotInfo {
if x != nil {
return x.Info
}
return nil
}
func (x *SnapshotDetails) GetFiles() []*FileInfo {
if x != nil {
return x.Files
}
return nil
}
// Запрос на получение списка снапшотов (можно добавить фильтры/пагинацию)
type ListSnapshotsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListSnapshotsRequest) Reset() {
*x = ListSnapshotsRequest{}
mi := &file_snapshot_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListSnapshotsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListSnapshotsRequest) ProtoMessage() {}
func (x *ListSnapshotsRequest) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListSnapshotsRequest.ProtoReflect.Descriptor instead.
func (*ListSnapshotsRequest) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{3}
}
// Ответ со списком снапшотов
type ListSnapshotsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Snapshots []*SnapshotInfo `protobuf:"bytes,1,rep,name=snapshots,proto3" json:"snapshots,omitempty"` // string next_page_token = 2;
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListSnapshotsResponse) Reset() {
*x = ListSnapshotsResponse{}
mi := &file_snapshot_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListSnapshotsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListSnapshotsResponse) ProtoMessage() {}
func (x *ListSnapshotsResponse) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListSnapshotsResponse.ProtoReflect.Descriptor instead.
func (*ListSnapshotsResponse) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{4}
}
func (x *ListSnapshotsResponse) GetSnapshots() []*SnapshotInfo {
if x != nil {
return x.Snapshots
}
return nil
}
// Запрос на получение деталей снапшота
type GetSnapshotDetailsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SnapshotId string `protobuf:"bytes,1,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` // ID нужного снапшота
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetSnapshotDetailsRequest) Reset() {
*x = GetSnapshotDetailsRequest{}
mi := &file_snapshot_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetSnapshotDetailsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetSnapshotDetailsRequest) ProtoMessage() {}
func (x *GetSnapshotDetailsRequest) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetSnapshotDetailsRequest.ProtoReflect.Descriptor instead.
func (*GetSnapshotDetailsRequest) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{5}
}
func (x *GetSnapshotDetailsRequest) GetSnapshotId() string {
if x != nil {
return x.SnapshotId
}
return ""
}
// Запрос на скачивание файла
type DownloadFileRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SnapshotId string `protobuf:"bytes,1,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` // ID снапшота
FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` // Путь к файлу внутри снапшота
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DownloadFileRequest) Reset() {
*x = DownloadFileRequest{}
mi := &file_snapshot_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DownloadFileRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DownloadFileRequest) ProtoMessage() {}
func (x *DownloadFileRequest) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DownloadFileRequest.ProtoReflect.Descriptor instead.
func (*DownloadFileRequest) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{6}
}
func (x *DownloadFileRequest) GetSnapshotId() string {
if x != nil {
return x.SnapshotId
}
return ""
}
func (x *DownloadFileRequest) GetFilePath() string {
if x != nil {
return x.FilePath
}
return ""
}
// Ответ (часть файла) при скачивании
type DownloadFileResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ChunkData []byte `protobuf:"bytes,1,opt,name=chunk_data,json=chunkData,proto3" json:"chunk_data,omitempty"` // Кусочек данных файла
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DownloadFileResponse) Reset() {
*x = DownloadFileResponse{}
mi := &file_snapshot_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DownloadFileResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DownloadFileResponse) ProtoMessage() {}
func (x *DownloadFileResponse) ProtoReflect() protoreflect.Message {
mi := &file_snapshot_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DownloadFileResponse.ProtoReflect.Descriptor instead.
func (*DownloadFileResponse) Descriptor() ([]byte, []int) {
return file_snapshot_proto_rawDescGZIP(), []int{7}
}
func (x *DownloadFileResponse) GetChunkData() []byte {
if x != nil {
return x.ChunkData
}
return nil
}
var File_snapshot_proto protoreflect.FileDescriptor
const file_snapshot_proto_rawDesc = "" +
"\n" +
"\x0esnapshot.proto\x12\n" +
"agate.grpc\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/api/annotations.proto\"u\n" +
"\bFileInfo\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x1d\n" +
"\n" +
"size_bytes\x18\x02 \x01(\x03R\tsizeBytes\x12\x1f\n" +
"\vsha256_hash\x18\x03 \x01(\tR\n" +
"sha256Hash\x12\x15\n" +
"\x06is_dir\x18\x04 \x01(\bR\x05isDir\"\x90\x01\n" +
"\fSnapshotInfo\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
"\tparent_id\x18\x03 \x01(\tR\bparentId\x12?\n" +
"\rcreation_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\fcreationTime\"k\n" +
"\x0fSnapshotDetails\x12,\n" +
"\x04info\x18\x01 \x01(\v2\x18.agate.grpc.SnapshotInfoR\x04info\x12*\n" +
"\x05files\x18\x02 \x03(\v2\x14.agate.grpc.FileInfoR\x05files\"\x16\n" +
"\x14ListSnapshotsRequest\"O\n" +
"\x15ListSnapshotsResponse\x126\n" +
"\tsnapshots\x18\x01 \x03(\v2\x18.agate.grpc.SnapshotInfoR\tsnapshots\"<\n" +
"\x19GetSnapshotDetailsRequest\x12\x1f\n" +
"\vsnapshot_id\x18\x01 \x01(\tR\n" +
"snapshotId\"S\n" +
"\x13DownloadFileRequest\x12\x1f\n" +
"\vsnapshot_id\x18\x01 \x01(\tR\n" +
"snapshotId\x12\x1b\n" +
"\tfile_path\x18\x02 \x01(\tR\bfilePath\"5\n" +
"\x14DownloadFileResponse\x12\x1d\n" +
"\n" +
"chunk_data\x18\x01 \x01(\fR\tchunkData2\x8a\x03\n" +
"\x0fSnapshotService\x12k\n" +
"\rListSnapshots\x12 .agate.grpc.ListSnapshotsRequest\x1a!.agate.grpc.ListSnapshotsResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/v1/snapshots\x12}\n" +
"\x12GetSnapshotDetails\x12%.agate.grpc.GetSnapshotDetailsRequest\x1a\x1b.agate.grpc.SnapshotDetails\"#\x82\xd3\xe4\x93\x02\x1d\x12\x1b/v1/snapshots/{snapshot_id}\x12\x8a\x01\n" +
"\fDownloadFile\x12\x1f.agate.grpc.DownloadFileRequest\x1a .agate.grpc.DownloadFileResponse\"5\x82\xd3\xe4\x93\x02/\x12-/v1/snapshots/{snapshot_id}/files/{file_path}0\x01B\x1cZ\x1aunprism.ru/KRBL/agate/grpcb\x06proto3"
var (
file_snapshot_proto_rawDescOnce sync.Once
file_snapshot_proto_rawDescData []byte
)
func file_snapshot_proto_rawDescGZIP() []byte {
file_snapshot_proto_rawDescOnce.Do(func() {
file_snapshot_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_snapshot_proto_rawDesc), len(file_snapshot_proto_rawDesc)))
})
return file_snapshot_proto_rawDescData
}
var file_snapshot_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_snapshot_proto_goTypes = []any{
(*FileInfo)(nil), // 0: agate.grpc.FileInfo
(*SnapshotInfo)(nil), // 1: agate.grpc.SnapshotInfo
(*SnapshotDetails)(nil), // 2: agate.grpc.SnapshotDetails
(*ListSnapshotsRequest)(nil), // 3: agate.grpc.ListSnapshotsRequest
(*ListSnapshotsResponse)(nil), // 4: agate.grpc.ListSnapshotsResponse
(*GetSnapshotDetailsRequest)(nil), // 5: agate.grpc.GetSnapshotDetailsRequest
(*DownloadFileRequest)(nil), // 6: agate.grpc.DownloadFileRequest
(*DownloadFileResponse)(nil), // 7: agate.grpc.DownloadFileResponse
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
}
var file_snapshot_proto_depIdxs = []int32{
8, // 0: agate.grpc.SnapshotInfo.creation_time:type_name -> google.protobuf.Timestamp
1, // 1: agate.grpc.SnapshotDetails.info:type_name -> agate.grpc.SnapshotInfo
0, // 2: agate.grpc.SnapshotDetails.files:type_name -> agate.grpc.FileInfo
1, // 3: agate.grpc.ListSnapshotsResponse.snapshots:type_name -> agate.grpc.SnapshotInfo
3, // 4: agate.grpc.SnapshotService.ListSnapshots:input_type -> agate.grpc.ListSnapshotsRequest
5, // 5: agate.grpc.SnapshotService.GetSnapshotDetails:input_type -> agate.grpc.GetSnapshotDetailsRequest
6, // 6: agate.grpc.SnapshotService.DownloadFile:input_type -> agate.grpc.DownloadFileRequest
4, // 7: agate.grpc.SnapshotService.ListSnapshots:output_type -> agate.grpc.ListSnapshotsResponse
2, // 8: agate.grpc.SnapshotService.GetSnapshotDetails:output_type -> agate.grpc.SnapshotDetails
7, // 9: agate.grpc.SnapshotService.DownloadFile:output_type -> agate.grpc.DownloadFileResponse
7, // [7:10] is the sub-list for method output_type
4, // [4:7] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_snapshot_proto_init() }
func file_snapshot_proto_init() {
if File_snapshot_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_snapshot_proto_rawDesc), len(file_snapshot_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_snapshot_proto_goTypes,
DependencyIndexes: file_snapshot_proto_depIdxs,
MessageInfos: file_snapshot_proto_msgTypes,
}.Build()
File_snapshot_proto = out.File
file_snapshot_proto_goTypes = nil
file_snapshot_proto_depIdxs = nil
}

286
grpc/snapshot.pb.gw.go Normal file
View File

@ -0,0 +1,286 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: snapshot.proto
/*
Package grpc is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package grpc
import (
"context"
"errors"
"io"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// Suppress "imported and not used" errors
var (
_ codes.Code
_ io.Reader
_ status.Status
_ = errors.New
_ = runtime.String
_ = utilities.NewDoubleArray
_ = metadata.Join
)
func request_SnapshotService_ListSnapshots_0(ctx context.Context, marshaler runtime.Marshaler, client SnapshotServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListSnapshotsRequest
metadata runtime.ServerMetadata
)
io.Copy(io.Discard, req.Body)
msg, err := client.ListSnapshots(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_SnapshotService_ListSnapshots_0(ctx context.Context, marshaler runtime.Marshaler, server SnapshotServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListSnapshotsRequest
metadata runtime.ServerMetadata
)
msg, err := server.ListSnapshots(ctx, &protoReq)
return msg, metadata, err
}
func request_SnapshotService_GetSnapshotDetails_0(ctx context.Context, marshaler runtime.Marshaler, client SnapshotServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetSnapshotDetailsRequest
metadata runtime.ServerMetadata
err error
)
io.Copy(io.Discard, req.Body)
val, ok := pathParams["snapshot_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "snapshot_id")
}
protoReq.SnapshotId, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "snapshot_id", err)
}
msg, err := client.GetSnapshotDetails(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_SnapshotService_GetSnapshotDetails_0(ctx context.Context, marshaler runtime.Marshaler, server SnapshotServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetSnapshotDetailsRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["snapshot_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "snapshot_id")
}
protoReq.SnapshotId, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "snapshot_id", err)
}
msg, err := server.GetSnapshotDetails(ctx, &protoReq)
return msg, metadata, err
}
func request_SnapshotService_DownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, client SnapshotServiceClient, req *http.Request, pathParams map[string]string) (SnapshotService_DownloadFileClient, runtime.ServerMetadata, error) {
var (
protoReq DownloadFileRequest
metadata runtime.ServerMetadata
err error
)
io.Copy(io.Discard, req.Body)
val, ok := pathParams["snapshot_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "snapshot_id")
}
protoReq.SnapshotId, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "snapshot_id", err)
}
val, ok = pathParams["file_path"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "file_path")
}
protoReq.FilePath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "file_path", err)
}
stream, err := client.DownloadFile(ctx, &protoReq)
if err != nil {
return nil, metadata, err
}
header, err := stream.Header()
if err != nil {
return nil, metadata, err
}
metadata.HeaderMD = header
return stream, metadata, nil
}
// RegisterSnapshotServiceHandlerServer registers the http handlers for service SnapshotService to "mux".
// UnaryRPC :call SnapshotServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterSnapshotServiceHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterSnapshotServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server SnapshotServiceServer) error {
mux.Handle(http.MethodGet, pattern_SnapshotService_ListSnapshots_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/agate.grpc.SnapshotService/ListSnapshots", runtime.WithHTTPPathPattern("/v1/snapshots"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_SnapshotService_ListSnapshots_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_SnapshotService_ListSnapshots_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_SnapshotService_GetSnapshotDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/agate.grpc.SnapshotService/GetSnapshotDetails", runtime.WithHTTPPathPattern("/v1/snapshots/{snapshot_id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_SnapshotService_GetSnapshotDetails_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_SnapshotService_GetSnapshotDetails_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_SnapshotService_DownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport")
_, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
})
return nil
}
// RegisterSnapshotServiceHandlerFromEndpoint is same as RegisterSnapshotServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterSnapshotServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.NewClient(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterSnapshotServiceHandler(ctx, mux, conn)
}
// RegisterSnapshotServiceHandler registers the http handlers for service SnapshotService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterSnapshotServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterSnapshotServiceHandlerClient(ctx, mux, NewSnapshotServiceClient(conn))
}
// RegisterSnapshotServiceHandlerClient registers the http handlers for service SnapshotService
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "SnapshotServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "SnapshotServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "SnapshotServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterSnapshotServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SnapshotServiceClient) error {
mux.Handle(http.MethodGet, pattern_SnapshotService_ListSnapshots_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/agate.grpc.SnapshotService/ListSnapshots", runtime.WithHTTPPathPattern("/v1/snapshots"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_SnapshotService_ListSnapshots_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_SnapshotService_ListSnapshots_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_SnapshotService_GetSnapshotDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/agate.grpc.SnapshotService/GetSnapshotDetails", runtime.WithHTTPPathPattern("/v1/snapshots/{snapshot_id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_SnapshotService_GetSnapshotDetails_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_SnapshotService_GetSnapshotDetails_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_SnapshotService_DownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/agate.grpc.SnapshotService/DownloadFile", runtime.WithHTTPPathPattern("/v1/snapshots/{snapshot_id}/files/{file_path}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_SnapshotService_DownloadFile_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_SnapshotService_DownloadFile_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_SnapshotService_ListSnapshots_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "snapshots"}, ""))
pattern_SnapshotService_GetSnapshotDetails_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "snapshots", "snapshot_id"}, ""))
pattern_SnapshotService_DownloadFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "snapshots", "snapshot_id", "files", "file_path"}, ""))
)
var (
forward_SnapshotService_ListSnapshots_0 = runtime.ForwardResponseMessage
forward_SnapshotService_GetSnapshotDetails_0 = runtime.ForwardResponseMessage
forward_SnapshotService_DownloadFile_0 = runtime.ForwardResponseStream
)

106
grpc/snapshot.proto Normal file
View File

@ -0,0 +1,106 @@
syntax = "proto3";
package agate.grpc;
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto"; // Добавлено для HTTP mapping
option go_package = "unprism.ru/KRBL/agate/grpc";
// Сервис для управления снапшотами
service SnapshotService {
// Получить список доступных снапшотов (краткая информация)
rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse) {
option (google.api.http) = {
get: "/v1/snapshots"
};
}
// Получить детальную информацию о снапшоте, включая список файлов с хешами
rpc GetSnapshotDetails(GetSnapshotDetailsRequest) returns (SnapshotDetails) {
option (google.api.http) = {
get: "/v1/snapshots/{snapshot_id}"
};
}
// Скачать конкретный файл из снапшота (потоковая передача)
rpc DownloadFile(DownloadFileRequest) returns (stream DownloadFileResponse) {
option (google.api.http) = {
get: "/v1/snapshots/{snapshot_id}/files/{file_path}"
};
}
// --- Методы для управления (опционально, можно не включать в публичный API клиента) ---
// Создать новый снапшот из директории (если серверу позволено инициировать)
// rpc CreateSnapshot(CreateSnapshotRequest) returns (Snapshot);
// Удалить снапшот (если требуется)
// rpc DeleteSnapshot(DeleteSnapshotRequest) returns (DeleteSnapshotResponse);
}
// Метаданные файла внутри снапшота
message FileInfo {
string path = 1; // Относительный путь файла внутри снапшота
int64 size_bytes = 2; // Размер файла в байтах
string sha256_hash = 3; // Хеш-сумма файла (SHA256)
bool is_dir = 4; // Является ли запись директорией
}
// Краткая информация о снапшоте
message SnapshotInfo {
string id = 1; // Уникальный ID снапшота (UUID)
string name = 2; // Имя снапшота
string parent_id = 3; // ID родительского снапшота (может быть пустым)
google.protobuf.Timestamp creation_time = 4; // Время создания
}
// Детальная информация о снапшоте
message SnapshotDetails {
SnapshotInfo info = 1; // Краткая информация
repeated FileInfo files = 2; // Список файлов в снапшоте
}
// Запрос на получение списка снапшотов (можно добавить фильтры/пагинацию)
message ListSnapshotsRequest {
// string filter_by_name = 1;
// int32 page_size = 2;
// string page_token = 3;
}
// Ответ со списком снапшотов
message ListSnapshotsResponse {
repeated SnapshotInfo snapshots = 1;
// string next_page_token = 2;
}
// Запрос на получение деталей снапшота
message GetSnapshotDetailsRequest {
string snapshot_id = 1; // ID нужного снапшота
}
// Запрос на скачивание файла
message DownloadFileRequest {
string snapshot_id = 1; // ID снапшота
string file_path = 2; // Путь к файлу внутри снапшота
}
// Ответ (часть файла) при скачивании
message DownloadFileResponse {
bytes chunk_data = 1; // Кусочек данных файла
}
// --- Сообщения для опциональных методов управления ---
/*
message CreateSnapshotRequest {
string source_path = 1; // Путь к директории на сервере
string name = 2;
string parent_id = 3; // Опционально
}
message DeleteSnapshotRequest {
string snapshot_id = 1;
}
message DeleteSnapshotResponse {
bool success = 1;
}
*/

211
grpc/snapshot_grpc.pb.go Normal file
View File

@ -0,0 +1,211 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v4.25.3
// source: snapshot.proto
package grpc
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
SnapshotService_ListSnapshots_FullMethodName = "/agate.grpc.SnapshotService/ListSnapshots"
SnapshotService_GetSnapshotDetails_FullMethodName = "/agate.grpc.SnapshotService/GetSnapshotDetails"
SnapshotService_DownloadFile_FullMethodName = "/agate.grpc.SnapshotService/DownloadFile"
)
// SnapshotServiceClient is the client API for SnapshotService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// Сервис для управления снапшотами
type SnapshotServiceClient interface {
// Получить список доступных снапшотов (краткая информация)
ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ListSnapshotsResponse, error)
// Получить детальную информацию о снапшоте, включая список файлов с хешами
GetSnapshotDetails(ctx context.Context, in *GetSnapshotDetailsRequest, opts ...grpc.CallOption) (*SnapshotDetails, error)
// Скачать конкретный файл из снапшота (потоковая передача)
DownloadFile(ctx context.Context, in *DownloadFileRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DownloadFileResponse], error)
}
type snapshotServiceClient struct {
cc grpc.ClientConnInterface
}
func NewSnapshotServiceClient(cc grpc.ClientConnInterface) SnapshotServiceClient {
return &snapshotServiceClient{cc}
}
func (c *snapshotServiceClient) ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ListSnapshotsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListSnapshotsResponse)
err := c.cc.Invoke(ctx, SnapshotService_ListSnapshots_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *snapshotServiceClient) GetSnapshotDetails(ctx context.Context, in *GetSnapshotDetailsRequest, opts ...grpc.CallOption) (*SnapshotDetails, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SnapshotDetails)
err := c.cc.Invoke(ctx, SnapshotService_GetSnapshotDetails_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *snapshotServiceClient) DownloadFile(ctx context.Context, in *DownloadFileRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DownloadFileResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &SnapshotService_ServiceDesc.Streams[0], SnapshotService_DownloadFile_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[DownloadFileRequest, DownloadFileResponse]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type SnapshotService_DownloadFileClient = grpc.ServerStreamingClient[DownloadFileResponse]
// SnapshotServiceServer is the server API for SnapshotService service.
// All implementations must embed UnimplementedSnapshotServiceServer
// for forward compatibility.
//
// Сервис для управления снапшотами
type SnapshotServiceServer interface {
// Получить список доступных снапшотов (краткая информация)
ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error)
// Получить детальную информацию о снапшоте, включая список файлов с хешами
GetSnapshotDetails(context.Context, *GetSnapshotDetailsRequest) (*SnapshotDetails, error)
// Скачать конкретный файл из снапшота (потоковая передача)
DownloadFile(*DownloadFileRequest, grpc.ServerStreamingServer[DownloadFileResponse]) error
mustEmbedUnimplementedSnapshotServiceServer()
}
// UnimplementedSnapshotServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedSnapshotServiceServer struct{}
func (UnimplementedSnapshotServiceServer) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListSnapshots not implemented")
}
func (UnimplementedSnapshotServiceServer) GetSnapshotDetails(context.Context, *GetSnapshotDetailsRequest) (*SnapshotDetails, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSnapshotDetails not implemented")
}
func (UnimplementedSnapshotServiceServer) DownloadFile(*DownloadFileRequest, grpc.ServerStreamingServer[DownloadFileResponse]) error {
return status.Errorf(codes.Unimplemented, "method DownloadFile not implemented")
}
func (UnimplementedSnapshotServiceServer) mustEmbedUnimplementedSnapshotServiceServer() {}
func (UnimplementedSnapshotServiceServer) testEmbeddedByValue() {}
// UnsafeSnapshotServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to SnapshotServiceServer will
// result in compilation errors.
type UnsafeSnapshotServiceServer interface {
mustEmbedUnimplementedSnapshotServiceServer()
}
func RegisterSnapshotServiceServer(s grpc.ServiceRegistrar, srv SnapshotServiceServer) {
// If the following call pancis, it indicates UnimplementedSnapshotServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&SnapshotService_ServiceDesc, srv)
}
func _SnapshotService_ListSnapshots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListSnapshotsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SnapshotServiceServer).ListSnapshots(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SnapshotService_ListSnapshots_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SnapshotServiceServer).ListSnapshots(ctx, req.(*ListSnapshotsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SnapshotService_GetSnapshotDetails_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetSnapshotDetailsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SnapshotServiceServer).GetSnapshotDetails(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SnapshotService_GetSnapshotDetails_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SnapshotServiceServer).GetSnapshotDetails(ctx, req.(*GetSnapshotDetailsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SnapshotService_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(DownloadFileRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(SnapshotServiceServer).DownloadFile(m, &grpc.GenericServerStream[DownloadFileRequest, DownloadFileResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type SnapshotService_DownloadFileServer = grpc.ServerStreamingServer[DownloadFileResponse]
// SnapshotService_ServiceDesc is the grpc.ServiceDesc for SnapshotService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var SnapshotService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "agate.grpc.SnapshotService",
HandlerType: (*SnapshotServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListSnapshots",
Handler: _SnapshotService_ListSnapshots_Handler,
},
{
MethodName: "GetSnapshotDetails",
Handler: _SnapshotService_GetSnapshotDetails_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "DownloadFile",
Handler: _SnapshotService_DownloadFile_Handler,
ServerStreams: true,
},
},
Metadata: "snapshot.proto",
}

32
hash/hash.go Normal file
View File

@ -0,0 +1,32 @@
package hash
import (
"crypto/sha256" // Используем SHA256
"encoding/hex" // Для преобразования хеша в строку
"fmt" // Для форматирования ошибок
"io" // Для копирования данных файла в хешер
"os" // Для открытия файла
)
// CalculateFileHash вычисляет SHA-256 хеш файла по указанному пути.
// Возвращает хеш в виде шестнадцатеричной строки и ошибку, если возникли проблемы
// при чтении файла или вычислении хеша.
func CalculateFileHash(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("failed to open file %s: %w", filePath, err)
}
defer file.Close()
hasher := sha256.New()
if _, err = io.Copy(hasher, file); err != nil {
return "", fmt.Errorf("failed to read file %s for hashing: %w", filePath, err)
}
hashBytes := hasher.Sum(nil)
hashString := hex.EncodeToString(hashBytes)
return hashString, nil
}

372
manager.go Normal file
View File

@ -0,0 +1,372 @@
package agate
import (
"archive/zip"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"unprism.ru/KRBL/agate/archive"
"unprism.ru/KRBL/agate/hash"
"unprism.ru/KRBL/agate/store"
)
type SnapshotManagerData struct {
metadataStore store.MetadataStore
blobStore store.BlobStore
}
func CreateSnapshotManager(metadataStore store.MetadataStore, blobStore store.BlobStore) (SnapshotManager, error) {
if metadataStore == nil || blobStore == nil {
return nil, errors.New("parameters can't be nil")
}
return &SnapshotManagerData{metadataStore, blobStore}, nil
}
func (data *SnapshotManagerData) CreateSnapshot(ctx context.Context, sourceDir string, name string, parentID string) (*store.Snapshot, error) {
// Validate source directory
info, err := os.Stat(sourceDir)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrSourceNotFound
}
return nil, fmt.Errorf("failed to access source directory: %w", err)
}
if !info.IsDir() {
return nil, ErrSourceNotDirectory
}
// Check if parent exists if specified
if parentID != "" {
_, err := data.metadataStore.GetSnapshotMetadata(ctx, parentID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, ErrParentNotFound
}
return nil, fmt.Errorf("failed to check parent snapshot: %w", err)
}
}
// Generate a unique ID for the snapshot
snapshotID := uuid.New().String()
// Create a temporary file for the archive
tempFile, err := os.CreateTemp("", "agate-snapshot-*.zip")
if err != nil {
return nil, fmt.Errorf("failed to create temporary file: %w", err)
}
tempFilePath := tempFile.Name()
tempFile.Close() // Close it as CreateArchive will reopen it
defer os.Remove(tempFilePath) // Clean up temp file after we're done
// Create archive of the source directory
if err := archive.CreateArchive(sourceDir, tempFilePath); err != nil {
return nil, fmt.Errorf("failed to create archive: %w", err)
}
// Scan the directory to collect file information
var files []store.FileInfo
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip the root directory itself
if path == sourceDir {
return nil
}
// Create relative path
relPath, err := filepath.Rel(sourceDir, path)
if err != nil {
return fmt.Errorf("failed to get relative path: %w", err)
}
fileInfo := store.FileInfo{
Path: filepath.ToSlash(relPath),
Size: info.Size(),
IsDir: info.IsDir(),
}
// Calculate hash for files (not directories)
if !info.IsDir() {
hash, err := hash.CalculateFileHash(path)
if err != nil {
return fmt.Errorf("failed to calculate hash for %s: %w", path, err)
}
fileInfo.SHA256 = hash
}
files = append(files, fileInfo)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to scan directory: %w", err)
}
// Open the archive file for reading
archiveFile, err := os.Open(tempFilePath)
if err != nil {
return nil, fmt.Errorf("failed to open archive file: %w", err)
}
defer archiveFile.Close()
// Store the blob
_, err = data.blobStore.StoreBlob(ctx, snapshotID, archiveFile)
if err != nil {
return nil, fmt.Errorf("failed to store blob: %w", err)
}
// Create snapshot metadata
snapshot := store.Snapshot{
ID: snapshotID,
Name: name,
ParentID: parentID,
CreationTime: time.Now(),
Files: files,
}
// Save metadata
if err := data.metadataStore.SaveSnapshotMetadata(ctx, snapshot); err != nil {
// If metadata save fails, try to clean up the blob
_ = data.blobStore.DeleteBlob(ctx, snapshotID)
return nil, fmt.Errorf("failed to save snapshot metadata: %w", err)
}
return &snapshot, nil
}
func (data *SnapshotManagerData) GetSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
if snapshotID == "" {
return nil, errors.New("snapshot ID cannot be empty")
}
// Retrieve snapshot metadata from the store
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to retrieve snapshot details: %w", err)
}
return snapshot, nil
}
func (data *SnapshotManagerData) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) {
// Retrieve list of snapshots from the metadata store
snapshots, err := data.metadataStore.ListSnapshotsMetadata(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list snapshots: %w", err)
}
return snapshots, nil
}
func (data *SnapshotManagerData) DeleteSnapshot(ctx context.Context, snapshotID string) error {
if snapshotID == "" {
return errors.New("snapshot ID cannot be empty")
}
// First check if the snapshot exists
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil {
if errors.Is(err, ErrNotFound) {
// If snapshot doesn't exist, return success (idempotent operation)
return nil
}
return fmt.Errorf("failed to check if snapshot exists: %w", err)
}
// Delete the metadata first
if err := data.metadataStore.DeleteSnapshotMetadata(ctx, snapshotID); err != nil {
return fmt.Errorf("failed to delete snapshot metadata: %w", err)
}
// Then delete the blob
if err := data.blobStore.DeleteBlob(ctx, snapshotID); err != nil {
// Note: We don't return here because we've already deleted the metadata
// and the blob store should handle the case where the blob doesn't exist
// Log the error instead
fmt.Printf("Warning: failed to delete snapshot blob: %v\n", err)
}
return nil
}
func (data *SnapshotManagerData) OpenFile(ctx context.Context, snapshotID string, filePath string) (io.ReadCloser, error) {
if snapshotID == "" {
return nil, errors.New("snapshot ID cannot be empty")
}
if filePath == "" {
return nil, errors.New("file path cannot be empty")
}
// First check if the snapshot exists
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to check if snapshot exists: %w", err)
}
// Get the blob path from the blob store
blobPath, err := data.blobStore.GetBlobPath(ctx, snapshotID)
if err != nil {
return nil, fmt.Errorf("failed to get blob path: %w", err)
}
// Create a pipe to stream the file content
pr, pw := io.Pipe()
// Extract the file in a goroutine to avoid blocking
go func() {
// Close the write end of the pipe when done
defer pw.Close()
// Extract the file from the archive
err := archive.ExtractFileFromArchive(blobPath, filePath, pw)
if err != nil {
if errors.Is(err, archive.ErrFileNotFoundInArchive) {
pw.CloseWithError(ErrFileNotFound)
return
}
pw.CloseWithError(fmt.Errorf("failed to extract file from archive: %w", err))
}
}()
return pr, nil
}
func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID string, path string) error {
if snapshotID == "" {
return errors.New("snapshot ID cannot be empty")
}
if path == "" {
return errors.New("target path cannot be empty")
}
// First check if the snapshot exists
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return ErrNotFound
}
return fmt.Errorf("failed to check if snapshot exists: %w", err)
}
// Get the blob path from the blob store
blobPath, err := data.blobStore.GetBlobPath(ctx, snapshotID)
if err != nil {
return fmt.Errorf("failed to get blob path: %w", err)
}
// Ensure the target directory exists
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create target directory: %w", err)
}
// Extract the archive to the target directory
if err := extractArchive(blobPath, path); err != nil {
return fmt.Errorf("failed to extract snapshot: %w", err)
}
return nil
}
// extractArchive extracts a ZIP archive to a target directory
func extractArchive(archivePath, targetDir string) error {
// Open the ZIP archive
zipReader, err := zip.OpenReader(archivePath)
if err != nil {
return fmt.Errorf("failed to open archive file: %w", err)
}
defer zipReader.Close()
// Extract each file
for _, file := range zipReader.File {
// Construct the full path for the file
filePath := filepath.Join(targetDir, file.Name)
// Check for path traversal attacks
if !strings.HasPrefix(filePath, targetDir) {
return fmt.Errorf("invalid file path in archive: %s", file.Name)
}
if file.FileInfo().IsDir() {
// Create directory
if err := os.MkdirAll(filePath, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", filePath, err)
}
continue
}
// Ensure the parent directory exists
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return fmt.Errorf("failed to create parent directory for %s: %w", filePath, err)
}
// Create the file
outFile, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("failed to create file %s: %w", filePath, err)
}
// Open the file in the archive
inFile, err := file.Open()
if err != nil {
outFile.Close()
return fmt.Errorf("failed to open file %s in archive: %w", file.Name, err)
}
// Copy the content
_, err = io.Copy(outFile, inFile)
outFile.Close()
inFile.Close()
if err != nil {
return fmt.Errorf("failed to copy content for %s: %w", filePath, err)
}
}
return nil
}
func (data *SnapshotManagerData) UpdateSnapshotMetadata(ctx context.Context, snapshotID string, newName string) error {
if snapshotID == "" {
return errors.New("snapshot ID cannot be empty")
}
if newName == "" {
return errors.New("new name cannot be empty")
}
// Get the current snapshot metadata
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return ErrNotFound
}
return fmt.Errorf("failed to get snapshot metadata: %w", err)
}
// Update the name
snapshot.Name = newName
// Save the updated metadata
if err := data.metadataStore.SaveSnapshotMetadata(ctx, *snapshot); err != nil {
return fmt.Errorf("failed to update snapshot metadata: %w", err)
}
return nil
}

53
snapshot.go Normal file
View File

@ -0,0 +1,53 @@
package agate
import (
"context"
"io"
"unprism.ru/KRBL/agate/store"
)
// SnapshotManager is an interface that defines operations for managing and interacting with snapshots.
type SnapshotManager interface {
// CreateSnapshot creates a new snapshot from the specified source directory, associating it with a given name and parent ID.
// Returns the created Snapshot with its metadata or an error if the process fails.
CreateSnapshot(ctx context.Context, sourceDir string, name string, parentID string) (*store.Snapshot, error)
// GetSnapshotDetails retrieves detailed metadata for a specific snapshot identified by its unique snapshotID.
// Returns a Snapshot object containing metadata
GetSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error)
// ListSnapshots retrieves a list of all available snapshots, returning their basic information as SnapshotInfo.
ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error)
// DeleteSnapshot removes a snapshot identified by snapshotID. Returns an error if the snapshot does not exist or cannot be deleted.
DeleteSnapshot(ctx context.Context, snapshotID string) error
// OpenFile retrieves and opens a file from the specified snapshot, returning a readable stream and an error, if any.
OpenFile(ctx context.Context, snapshotID string, filePath string) (io.ReadCloser, error)
// ExtractSnapshot extracts the contents of a specified snapshot to a target directory at the given path.
// Returns an error if the snapshot ID is invalid or the extraction fails.
ExtractSnapshot(ctx context.Context, snapshotID string, path string) error
// UpdateSnapshotMetadata updates the metadata of an existing snapshot, allowing changes to its name.
UpdateSnapshotMetadata(ctx context.Context, snapshotID string, newName string) error
}
type SnapshotServer interface {
// Start initializes and begins the server's operation, handling incoming requests or processes within the provided context.
Start(ctx context.Context) error
// Stop gracefully shuts down the server, releasing any allocated resources and ensuring all operations are completed.
Stop(ctx context.Context) error
}
type SnapshotClient interface {
// ListSnapshots retrieves a list of snapshots containing basic metadata, such as ID, name, parent ID, and creation time.
ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error)
// FetchSnapshotDetails retrieves detailed metadata about a specific snapshot identified by snapshotID.
FetchSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error)
// DownloadSnapshot retrieves the snapshot content for the given snapshotID and returns it as an io.ReadCloser.
DownloadSnapshot(ctx context.Context, snapshotID string) (io.ReadCloser, error)
}

View File

@ -0,0 +1,108 @@
package filesystem
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"unprism.ru/KRBL/agate"
"unprism.ru/KRBL/agate/store"
)
const blobExtension = ".zip"
// fileSystemStore реализует интерфейс store.BlobStore с использованием локальной файловой системы.
type fileSystemStore struct {
baseDir string // Директория для хранения блобов (архивов)
}
// NewFileSystemStore создает новое хранилище блобов в указанной директории.
func NewFileSystemStore(baseDir string) (store.BlobStore, error) {
// Убедимся, что директория существует
if err := os.MkdirAll(baseDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create base directory %s for filesystem blob store: %w", baseDir, err)
}
return &fileSystemStore{baseDir: baseDir}, nil
}
// getBlobPath формирует полный путь к файлу блоба.
func (fs *fileSystemStore) getBlobPath(snapshotID string) string {
// Используем ID снапшота в качестве имени файла
return filepath.Join(fs.baseDir, snapshotID+blobExtension)
}
// StoreBlob сохраняет данные из reader в файл в baseDir.
func (fs *fileSystemStore) StoreBlob(ctx context.Context, snapshotID string, reader io.Reader) (string, error) {
blobPath := fs.getBlobPath(snapshotID)
// Создаем или перезаписываем файл
file, err := os.Create(blobPath)
if err != nil {
return "", fmt.Errorf("failed to create blob file %s: %w", blobPath, err)
}
defer file.Close() // Гарантируем закрытие файла
// Копируем данные из ридера в файл
_, err = io.Copy(file, reader)
if err != nil {
// Если произошла ошибка копирования, удаляем неполный файл
os.Remove(blobPath)
return "", fmt.Errorf("failed to write data to blob file %s: %w", blobPath, err)
}
// Возвращаем путь к созданному файлу
return blobPath, nil
}
// RetrieveBlob открывает файл блоба и возвращает его как io.ReadCloser.
func (fs *fileSystemStore) RetrieveBlob(ctx context.Context, snapshotID string) (io.ReadCloser, error) {
blobPath := fs.getBlobPath(snapshotID)
// Открываем файл для чтения
file, err := os.Open(blobPath)
if err != nil {
if os.IsNotExist(err) {
// Если файл не найден, возвращаем кастомную ошибку
return nil, agate.ErrNotFound
}
return nil, fmt.Errorf("failed to open blob file %s: %w", blobPath, err)
}
// Возвращаем открытый файл (*os.File реализует io.ReadCloser)
return file, nil
}
// DeleteBlob удаляет файл блоба из файловой системы.
func (fs *fileSystemStore) DeleteBlob(ctx context.Context, snapshotID string) error {
blobPath := fs.getBlobPath(snapshotID)
// Удаляем файл
err := os.Remove(blobPath)
if err != nil {
if os.IsNotExist(err) {
// Если файл и так не существует, это не ошибка
return nil
}
// Если произошла другая ошибка при удалении
return fmt.Errorf("failed to delete blob file %s: %w", blobPath, err)
}
return nil
}
// GetBlobPath возвращает путь к файлу блоба, если он существует.
func (fs *fileSystemStore) GetBlobPath(ctx context.Context, snapshotID string) (string, error) {
blobPath := fs.getBlobPath(snapshotID)
// Проверяем существование файла
if _, err := os.Stat(blobPath); err != nil {
if os.IsNotExist(err) {
return "", agate.ErrNotFound
}
return "", fmt.Errorf("failed to stat blob file %s: %w", blobPath, err)
}
// Файл существует, возвращаем путь
return blobPath, nil
}

243
store/sqlite/sqlite.go Normal file
View File

@ -0,0 +1,243 @@
package sqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3"
"os"
"path/filepath"
"time"
"unprism.ru/KRBL/agate"
"unprism.ru/KRBL/agate/store"
)
const (
createSnapshotsTableSQL = `
CREATE TABLE IF NOT EXISTS snapshots (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
parent_id TEXT,
creation_time DATETIME NOT NULL,
files_json TEXT -- Храним список файлов как JSON blob
);`
)
// sqliteStore реализует интерфейс store.MetadataStore с использованием SQLite.
type sqliteStore struct {
db *sql.DB
}
// NewSQLiteStore создает и инициализирует новое хранилище метаданных на SQLite.
func NewSQLiteStore(dbPath string) (store.MetadataStore, error) {
// Убедимся, что директория для файла БД существует
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create directory for sqlite db %s: %w", dir, err)
}
// Открываем или создаем файл БД SQLite
// Опции _journal=WAL и _busy_timeout=5000 могут улучшить производительность при конкурентном доступе
dsn := fmt.Sprintf("file:%s?_journal=WAL&_busy_timeout=5000&_foreign_keys=on", dbPath)
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open sqlite database %s: %w", dbPath, err)
}
// Проверяем соединение
if err := db.PingContext(context.Background()); err != nil { // Используем Background т.к. это инициализация
db.Close()
return nil, fmt.Errorf("failed to ping sqlite database %s: %w", dbPath, err)
}
// Применяем схему БД
if err := createSchema(context.Background(), db); err != nil { // Используем Background т.к. это инициализация
db.Close()
return nil, fmt.Errorf("failed to apply sqlite schema: %w", err)
}
return &sqliteStore{db: db}, nil
}
// createSchema создает необходимые таблицы, если они не существуют.
func createSchema(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, createSnapshotsTableSQL)
if err != nil {
return fmt.Errorf("failed to execute schema creation: %w", err)
}
return nil
}
// Close закрывает соединение с БД.
func (s *sqliteStore) Close() error {
if s.db != nil {
return s.db.Close()
}
return nil
}
// SaveSnapshotMetadata сохраняет метаданные снапшота в БД.
func (s *sqliteStore) SaveSnapshotMetadata(ctx context.Context, snap store.Snapshot) error {
// Сериализуем список файлов в JSON
filesJSON, err := json.Marshal(snap.Files)
if err != nil {
return fmt.Errorf("failed to marshal files to JSON for snapshot %s: %w", snap.ID, err)
}
// Используем INSERT OR REPLACE для атомарной вставки или обновления
query := `
INSERT OR REPLACE INTO snapshots (id, name, parent_id, creation_time, files_json)
VALUES (?, ?, ?, ?, ?);
`
_, err = s.db.ExecContext(ctx, query,
snap.ID,
snap.Name,
sql.NullString{String: snap.ParentID, Valid: snap.ParentID != ""}, // Используем NullString для опционального parent_id
snap.CreationTime.UTC(), // Сохраняем в UTC
string(filesJSON),
)
if err != nil {
return fmt.Errorf("failed to save snapshot %s metadata: %w", snap.ID, err)
}
return nil
}
// GetSnapshotMetadata извлекает метаданные снапшота из БД.
func (s *sqliteStore) GetSnapshotMetadata(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
query := `
SELECT id, name, parent_id, creation_time, files_json
FROM snapshots
WHERE id = ?;
`
row := s.db.QueryRowContext(ctx, query, snapshotID)
var snap store.Snapshot
var parentID sql.NullString
var filesJSON string
var creationTimeStr string // Читаем время как строку, потом парсим
err := row.Scan(
&snap.ID,
&snap.Name,
&parentID,
&creationTimeStr,
&filesJSON,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Если запись не найдена, возвращаем кастомную ошибку
return nil, agate.ErrNotFound
}
return nil, fmt.Errorf("failed to query snapshot %s: %w", snapshotID, err)
}
// Устанавливаем ParentID, если он не NULL
snap.ParentID = parentID.String
// Парсим время из строки (SQLite хранит DATETIME как текст)
// Используем формат, совместимый с SQLite и Go's time.RFC3339Nano, или стандартный SQLite формат
// 'YYYY-MM-DD HH:MM:SS' - требует парсинга с нужным layout
// Проще сохранять и читать как RFC3339 или Unix timestamp. Мы сохраняли как UTC().
// Попробуем стандартный формат SQLite
const sqliteLayout = "2006-01-02 15:04:05" // Стандартный формат SQLite DATETIME без таймзоны
t, parseErr := time.Parse(sqliteLayout, creationTimeStr)
if parseErr != nil {
// Попробуем формат с долями секунд, если первый не сработал
const sqliteLayoutWithMs = "2006-01-02 15:04:05.999999999"
t, parseErr = time.Parse(sqliteLayoutWithMs, creationTimeStr)
if parseErr != nil {
// Попробуем RFC3339, если сохраняли как UTC().Format(time.RFC3339)
t, parseErr = time.Parse(time.RFC3339, creationTimeStr)
if parseErr != nil {
return nil, fmt.Errorf("failed to parse creation time '%s' for snapshot %s: %w", creationTimeStr, snapshotID, parseErr)
}
}
}
snap.CreationTime = t.UTC() // Сохраняем как UTC
// Десериализуем список файлов из JSON
if err := json.Unmarshal([]byte(filesJSON), &snap.Files); err != nil {
return nil, fmt.Errorf("failed to unmarshal files JSON for snapshot %s: %w", snapshotID, err)
}
return &snap, nil
}
// ListSnapshotsMetadata извлекает краткую информацию обо всех снапшотах.
func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context) ([]store.SnapshotInfo, error) {
// Simplified implementation to debug the issue
fmt.Println("ListSnapshotsMetadata called")
// Get all snapshot IDs first
query := `SELECT id FROM snapshots ORDER BY creation_time DESC;`
fmt.Println("Executing query:", query)
rows, err := s.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query snapshot IDs: %w", err)
}
defer rows.Close()
var snapshots []store.SnapshotInfo
// For each ID, get the full snapshot details
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("failed to scan snapshot ID: %w", err)
}
// Get the full snapshot details
snapshot, err := s.GetSnapshotMetadata(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get snapshot details for ID %s: %w", id, err)
}
// Convert to SnapshotInfo
info := store.SnapshotInfo{
ID: snapshot.ID,
Name: snapshot.Name,
ParentID: snapshot.ParentID,
CreationTime: snapshot.CreationTime,
}
snapshots = append(snapshots, info)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating snapshot IDs: %w", err)
}
// If no snapshots found, return an empty slice
if len(snapshots) == 0 {
fmt.Println("No snapshots found")
return []store.SnapshotInfo{}, nil
}
fmt.Printf("Found %d snapshots\n", len(snapshots))
return snapshots, nil
}
// DeleteSnapshotMetadata удаляет метаданные снапшота.
func (s *sqliteStore) DeleteSnapshotMetadata(ctx context.Context, snapshotID string) error {
query := `DELETE FROM snapshots WHERE id = ?;`
result, err := s.db.ExecContext(ctx, query, snapshotID)
if err != nil {
return fmt.Errorf("failed to delete snapshot %s metadata: %w", snapshotID, err)
}
// Проверяем, была ли запись реально удалена (опционально)
_, err = result.RowsAffected()
if err != nil {
// Не критично, если не можем получить количество удаленных строк
fmt.Printf("Warning: could not get rows affected after deleting snapshot %s: %v\n", snapshotID, err)
}
// fmt.Printf("Deleted %d metadata rows for snapshot %s\n", rowsAffected, snapshotID)
return nil // Не возвращаем ошибку, если запись не найдена
}

74
store/store.go Normal file
View File

@ -0,0 +1,74 @@
package store
import (
"context"
"io"
"time"
)
// FileInfo represents metadata and attributes of a file or directory.
type FileInfo struct {
Path string // Path represents the relative or absolute location of the file or directory in the filesystem.
Size int64 // Size represents the size of the file in bytes.
IsDir bool // IsDir indicates whether the FileInfo represents a directory.
SHA256 string // SHA256 represents the SHA-256 checksum of the file for integrity verification.
}
// Snapshot represents a point-in-time capture of file system metadata and hierarchy.
type Snapshot struct {
ID string // ID is the unique identifier of the snapshot.
Name string // Name is the user-defined name of the snapshot.
ParentID string // ParentID is the unique identifier of the parent snapshot, if one exists.
CreationTime time.Time // CreationTime is the timestamp indicating when the snapshot was created.
Files []FileInfo // Files represents a list of file metadata contained in the snapshot.
}
// SnapshotInfo provides basic metadata about a snapshot, including its ID, name, parent snapshot, and creation time.
type SnapshotInfo struct {
ID string // Уникальный идентификатор
Name string // Имя снапшота
ParentID string // ID родительского снапшота
CreationTime time.Time // Время создания
}
// MetadataStore определяет интерфейс для хранения и извлечения метаданных снапшотов.
type MetadataStore interface {
// SaveSnapshotMetadata сохраняет полные метаданные снапшота, включая список файлов.
// Если снапшот с таким ID уже существует, он должен быть перезаписан.
SaveSnapshotMetadata(ctx context.Context, snap Snapshot) error
// GetSnapshotMetadata извлекает полные метаданные снапшота по его ID.
// Возвращает agate.ErrNotFound, если снапшот не найден.
GetSnapshotMetadata(ctx context.Context, snapshotID string) (*Snapshot, error)
// ListSnapshotsMetadata извлекает краткую информацию обо всех снапшотах.
ListSnapshotsMetadata(ctx context.Context) ([]SnapshotInfo, error)
// DeleteSnapshotMetadata удаляет метаданные снапшота по его ID.
// Не должен возвращать ошибку, если снапшот не найден.
DeleteSnapshotMetadata(ctx context.Context, snapshotID string) error
// Close закрывает соединение с хранилищем метаданных.
Close() error
}
// BlobStore определяет интерфейс для хранения и извлечения самих данных снапшотов (архивов).
type BlobStore interface {
// StoreBlob сохраняет данные из reader как блоб для указанного snapshotID.
// Возвращает путь или идентификатор сохраненного блоба и ошибку.
StoreBlob(ctx context.Context, snapshotID string, reader io.Reader) (path string, err error)
// RetrieveBlob возвращает io.ReadCloser для чтения содержимого блоба по snapshotID.
// Вызывающая сторона ОБЯЗАНА закрыть ReadCloser.
// Возвращает snapshot.ErrNotFound, если блоб не найден.
RetrieveBlob(ctx context.Context, snapshotID string) (io.ReadCloser, error)
// DeleteBlob удаляет блоб, связанный с snapshotID.
// Не должен возвращать ошибку, если блоб не найден.
DeleteBlob(ctx context.Context, snapshotID string) error
// GetBlobPath возвращает путь к файлу блоба в файловой системе.
// Это может быть полезно для функций пакета archive, которые работают с путями.
// Возвращает agate.ErrNotFound, если блоб не найден.
GetBlobPath(ctx context.Context, snapshotID string) (string, error)
}

42
stores/stores.go Normal file
View File

@ -0,0 +1,42 @@
package stores
import (
"fmt"
"path/filepath"
"unprism.ru/KRBL/agate/store"
"unprism.ru/KRBL/agate/store/filesystem"
"unprism.ru/KRBL/agate/store/sqlite"
)
// NewDefaultMetadataStore creates a new SQLite-based metadata store.
func NewDefaultMetadataStore(metadataDir string) (store.MetadataStore, error) {
dbPath := filepath.Join(metadataDir, "snapshots.db")
return sqlite.NewSQLiteStore(dbPath)
}
// NewDefaultBlobStore creates a new filesystem-based blob store.
func NewDefaultBlobStore(blobsDir string) (store.BlobStore, error) {
return filesystem.NewFileSystemStore(blobsDir)
}
// InitDefaultStores initializes both metadata and blob stores with default implementations.
// Returns the initialized stores or an error if initialization fails.
func InitDefaultStores(baseDir string) (store.MetadataStore, store.BlobStore, error) {
metadataDir := filepath.Join(baseDir, "metadata")
blobsDir := filepath.Join(baseDir, "blobs")
metadataStore, err := NewDefaultMetadataStore(metadataDir)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize metadata store: %w", err)
}
blobStore, err := NewDefaultBlobStore(blobsDir)
if err != nil {
// Clean up if blob store initialization fails
metadataStore.Close()
return nil, nil, fmt.Errorf("failed to initialize blob store: %w", err)
}
return metadataStore, blobStore, nil
}