Files
Agate/archive/archive.go
2025-11-21 14:46:16 +03:00

254 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
// CreateArchiveWithProgress creates a ZIP archive with progress reporting.
// onProgress is called with the current number of bytes written and the total size.
func CreateArchiveWithProgress(sourceDir, targetPath string, onProgress func(current, total int64)) 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)
}
// Calculate total size
var totalSize int64
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
totalSize += info.Size()
}
return nil
})
if err != nil {
return fmt.Errorf("failed to calculate total size of %s: %w", sourceDir, err)
}
// Create file for ZIP archive
outFile, err := os.Create(targetPath)
if err != nil {
return fmt.Errorf("failed to create target archive file %s: %w", targetPath, err)
}
defer outFile.Close()
// Create zip.Writer
zipWriter := zip.NewWriter(outFile)
defer zipWriter.Close()
var currentSize int64
// Recursively walk 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)
}
// Skip sourceDir itself
if filePath == sourceDir {
return nil
}
// Create relative path
relativePath := strings.TrimPrefix(filePath, sourceDir+string(filepath.Separator))
relativePath = filepath.ToSlash(relativePath)
// Check if directory
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
}
// Open file for reading
fileToArchive, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open file %s for archiving: %w", filePath, err)
}
defer fileToArchive.Close()
// Create archive entry
zipEntryWriter, err := zipWriter.Create(relativePath)
if err != nil {
return fmt.Errorf("failed to create entry %s in archive: %w", relativePath, err)
}
// Copy content
n, err := io.Copy(zipEntryWriter, fileToArchive)
if err != nil {
return fmt.Errorf("failed to copy file content %s to archive: %w", filePath, err)
}
currentSize += n
if onProgress != nil {
onProgress(currentSize, totalSize)
}
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
}