package filesystem import ( "context" "fmt" "io" "os" "path/filepath" "gitea.unprism.ru/KRBL/Agate/store" ) const blobExtension = ".zip" // fileSystemStore реализует интерфейс store.BlobStore с использованием локальной файловой системы. type fileSystemStore struct { baseDir string // Директория для хранения блобов (архивов) activeDir 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) } // Создаем директорию для активных операций внутри базовой директории activeDir := filepath.Join(baseDir, "active") if err := os.MkdirAll(activeDir, 0755); err != nil { return nil, fmt.Errorf("failed to create active directory %s for filesystem blob store: %w", activeDir, err) } return &fileSystemStore{ baseDir: baseDir, activeDir: activeDir, }, 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, store.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 "", store.ErrNotFound } return "", fmt.Errorf("failed to stat blob file %s: %w", blobPath, err) } // Файл существует, возвращаем путь return blobPath, nil } // GetActiveDir возвращает путь к директории для активных операций. func (fs *fileSystemStore) GetBaseDir() string { return fs.baseDir } // GetActiveDir возвращает путь к директории для активных операций. func (fs *fileSystemStore) GetActiveDir() string { return fs.activeDir } // CleanActiveDir очищает директорию для активных операций. // Это полезно перед началом новых операций, чтобы избежать конфликтов. func (fs *fileSystemStore) CleanActiveDir(ctx context.Context) error { // Удаляем все файлы в активной директории, но сохраняем саму директорию entries, err := os.ReadDir(fs.activeDir) if err != nil { return fmt.Errorf("failed to read active directory: %w", err) } for _, entry := range entries { path := filepath.Join(fs.activeDir, entry.Name()) if err := os.RemoveAll(path); err != nil { return fmt.Errorf("failed to remove %s from active directory: %w", path, err) } } return nil }