2023-03-11 14:13:35 +05:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"archive/tar"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/klauspost/compress/zstd"
|
|
|
|
|
"github.com/nxshock/progressmessage"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Mask struct {
|
|
|
|
|
Path string
|
|
|
|
|
|
|
|
|
|
// Маски имени файла
|
|
|
|
|
MaskList []string
|
|
|
|
|
|
|
|
|
|
// Вкючать файлы в покаталогах
|
|
|
|
|
Recursive bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FindAll возвращает индекс файлов, совпавших по маске
|
|
|
|
|
func (b *Config) FindAll(mask string) (*Index, error) {
|
|
|
|
|
b.logf(LogLevelDebug, "Поиск маски %s...", mask)
|
2023-03-17 22:17:58 +05:00
|
|
|
|
index, err := b.index(true)
|
2023-03-11 14:13:35 +05:00
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("index: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := &Index{Files: make(map[string]FileHistory)}
|
|
|
|
|
|
|
|
|
|
for path, info := range index.Files {
|
|
|
|
|
matched, err := filepath.Match(strings.ToLower(mask), strings.ToLower(filepath.ToSlash(path)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("filepath.Match: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if matched {
|
|
|
|
|
result.Files[path] = append(result.Files[path], info...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IncrementalBackup выполняет инкрементальный бекап.
|
|
|
|
|
// В случае, если бекап выполняется впервые, выполняется полный бекап.
|
|
|
|
|
func (b *Config) IncrementalBackup() error {
|
2023-03-17 22:17:58 +05:00
|
|
|
|
index, err := b.index(false)
|
2023-03-11 14:13:35 +05:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.doBackup(index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FullBackup выполняет полное резервное копирование
|
|
|
|
|
func (b *Config) FullBackup() error {
|
|
|
|
|
return b.doBackup(nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Config) doBackup(index *Index) error {
|
|
|
|
|
var suffix string
|
|
|
|
|
if index == nil || index.ItemCount() == 0 {
|
|
|
|
|
suffix = "f" // Full backup - полный бекап
|
|
|
|
|
} else {
|
|
|
|
|
suffix = "i" // Инкрементальный бекап
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-17 22:17:58 +05:00
|
|
|
|
filePath := filepath.Join(filepath.Dir(b.filePath), b.FileName+"_"+time.Now().Local().Format(defaulFileNameTimeFormat)+suffix+defaultExt)
|
2023-03-11 14:13:35 +05:00
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
filePath, err = filepath.Abs(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при создании файла архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
b.logf(LogLevelProgress, "Создание нового файла бекапа %s...", filePath)
|
|
|
|
|
|
|
|
|
|
if _, err = os.Stat(filepath.Dir(filePath)); os.IsNotExist(err) {
|
|
|
|
|
err = os.MkdirAll(filepath.Dir(filePath), 0644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при создании каталога для архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resultArchiveFile, err := os.Create(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при создании файла архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compressor, err := zstd.NewWriter(resultArchiveFile, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при создании инициализации архиватора: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tarWriter := tar.NewWriter(compressor)
|
|
|
|
|
|
|
|
|
|
b.log(LogLevelInfo, "Копирование файлов...")
|
|
|
|
|
|
|
|
|
|
pm := progressmessage.New("Добавлено %d файлов, %s...")
|
|
|
|
|
if b.Logger.MinimalLogLevel <= LogLevelProgress {
|
|
|
|
|
pm.Start()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i := 0 // счётчик обработанных файлов
|
|
|
|
|
addSize := int64(0) // добавлено байт
|
|
|
|
|
for k := range b.planChan(index) {
|
|
|
|
|
i++
|
|
|
|
|
addSize += k.Info.Size()
|
|
|
|
|
err := b.addFileToTarWriter(k.SourcePath, tarWriter)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.logf(LogLevelWarning, "ошибка при добавлении файла %s: %v\n", k.SourcePath, err)
|
|
|
|
|
if b.StopOnAnyError {
|
|
|
|
|
compressor.Close()
|
|
|
|
|
resultArchiveFile.Close()
|
|
|
|
|
os.Remove(filePath)
|
|
|
|
|
return fmt.Errorf("ошибка при добавлении файла в архив: %v", err) // TODO: организовать закрытие и удаление частичного файла
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.Logger.MinimalLogLevel <= LogLevelProgress {
|
|
|
|
|
pm.Update(i, sizeToApproxHuman(addSize))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.Logger.MinimalLogLevel <= LogLevelProgress {
|
|
|
|
|
pm.Stop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = tarWriter.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
compressor.Close()
|
|
|
|
|
resultArchiveFile.Close()
|
|
|
|
|
os.Remove(filePath)
|
|
|
|
|
return fmt.Errorf("ошибка при закрытии tar-архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = compressor.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
resultArchiveFile.Close()
|
|
|
|
|
os.Remove(filePath)
|
|
|
|
|
return fmt.Errorf("ошибка при закрытии архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.Logger.MinimalLogLevel <= LogLevelProgress {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\rДобавлено %d файлов, %s.\n", i, sizeToApproxHuman(addSize))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = resultArchiveFile.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при закрытии файла архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// если не было обновлений, удалить пустой файл
|
|
|
|
|
if i == 0 {
|
|
|
|
|
os.Remove(filePath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Config) fileList(fileNames chan File) {
|
|
|
|
|
errorCount := 0
|
|
|
|
|
|
|
|
|
|
for _, v := range b.Masks {
|
|
|
|
|
if v.Recursive {
|
|
|
|
|
err := filepath.WalkDir(v.Path, func(path string, d fs.DirEntry, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
errorCount++
|
|
|
|
|
b.logf(LogLevelCritical, "Ошибка при поиске файлов: %v\n", err)
|
|
|
|
|
if b.StopOnAnyError {
|
|
|
|
|
return fmt.Errorf("ошибка при переборе файлов: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if d.IsDir() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !v.Recursive && filepath.Dir(path) != v.Path {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fileName := filepath.Base(path)
|
|
|
|
|
fileName := path // TODO: тестирование - маска действует на весь путь
|
|
|
|
|
|
|
|
|
|
if isFileMatchMasks(v.MaskList, fileName) {
|
|
|
|
|
if !isFileMatchMasks(b.GlobalExcludeMasks, fileName) {
|
|
|
|
|
info, err := os.Stat(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errorCount++
|
|
|
|
|
b.logf(LogLevelCritical, "Ошибка при получении информации о файле: %v\n", err)
|
|
|
|
|
if b.StopOnAnyError {
|
|
|
|
|
return fmt.Errorf("ошибка при получении информации о файле: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file := File{
|
|
|
|
|
SourcePath: path,
|
|
|
|
|
DestinationPath: filepath.ToSlash(path),
|
|
|
|
|
Info: info}
|
|
|
|
|
fileNames <- file
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.logf(LogLevelCritical, "Ошибка при получении списка файлов: %v\n", err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
allFilesAndDirs, err := filepath.Glob(filepath.Join(v.Path, "*"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
errorCount++
|
|
|
|
|
b.logf(LogLevelCritical, "Ошибка при получении списка файлов: %v\n", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, fileOrDirPath := range allFilesAndDirs {
|
|
|
|
|
info, err := os.Stat(fileOrDirPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errorCount++
|
|
|
|
|
b.logf(LogLevelCritical, "Ошибка при получении информации об объекте: %v\n", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if info.IsDir() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//fileName := filepath.Base(fileOrDirPath)
|
|
|
|
|
fileName := fileOrDirPath // TODO: тестирование, маска должна накладываться на путь
|
|
|
|
|
|
|
|
|
|
if isFileMatchMasks(v.MaskList, fileName) {
|
|
|
|
|
if !isFileMatchMasks(b.GlobalExcludeMasks, fileName) {
|
|
|
|
|
file := File{
|
|
|
|
|
SourcePath: fileOrDirPath,
|
|
|
|
|
DestinationPath: filepath.ToSlash(fileOrDirPath),
|
|
|
|
|
Info: info}
|
|
|
|
|
fileNames <- file
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if errorCount > 0 {
|
|
|
|
|
b.logf(LogLevelCritical, "Ошибок: %d\n", errorCount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(fileNames)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isFileMatchMasks(masks []string, fileName string) bool {
|
|
|
|
|
for _, mask := range masks {
|
|
|
|
|
if match, _ := filepath.Match(filepath.ToSlash(mask), filepath.ToSlash(fileName)); match {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Config) addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
|
|
|
|
|
b.logf(LogLevelDebug, "Добавление файла %s...\n", filePath)
|
|
|
|
|
|
|
|
|
|
file, err := os.Open(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Could not open file '%s', got error '%s'", filePath, err.Error())
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
stat, err := file.Stat()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Could not get stat for file '%s', got error '%s'", filePath, err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
header := &tar.Header{
|
|
|
|
|
Format: tar.FormatGNU,
|
|
|
|
|
Name: filepath.ToSlash(filePath),
|
|
|
|
|
Size: stat.Size(),
|
|
|
|
|
ModTime: stat.ModTime()}
|
|
|
|
|
|
|
|
|
|
err = tarWriter.WriteHeader(header)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Could not write header for file '%s', got error '%s'", filePath, err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = io.Copy(tarWriter, file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetFileWithTime возвращает содержимое файла на указанную дату.
|
|
|
|
|
func (b *Config) GetFileWithTime(path string, t time.Time, w io.Writer) error {
|
2023-03-17 22:17:58 +05:00
|
|
|
|
index, err := b.index(true)
|
2023-03-11 14:13:35 +05:00
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при построении индекса: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file, err := index.GetFileWithTime(path, t)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при получении информации из индекса: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, err := os.Open(file.ArchiveFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при чтении файла архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
decoder, err := zstd.NewReader(f)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при инициализации разархиватора: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer decoder.Close()
|
|
|
|
|
|
|
|
|
|
tarReader := tar.NewReader(decoder)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
header, err := tarReader.Next()
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при чтении tar-содержимого: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if header.Name == path {
|
|
|
|
|
_, err = io.Copy(w, tarReader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("ошибка при извлечении файла из tar-архива: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-17 22:17:58 +05:00
|
|
|
|
// fullIndex - true = все файлы, false = только от последнего полного
|
|
|
|
|
func (b *Config) index(fullIndex bool) (*Index, error) {
|
2023-03-11 14:13:35 +05:00
|
|
|
|
b.logf(LogLevelInfo, "Построение индекса текущего архива из %s...", filepath.Dir(b.filePath))
|
2023-03-17 22:17:58 +05:00
|
|
|
|
allFileMask := filepath.Join(filepath.Dir(b.filePath), b.FileName+"*"+defaultExt)
|
|
|
|
|
onlyFullBackupFileMask := filepath.Join(filepath.Dir(b.filePath), b.FileName+"*f"+defaultExt)
|
2023-03-11 14:13:35 +05:00
|
|
|
|
|
2023-03-17 22:17:58 +05:00
|
|
|
|
// Get last full backup name
|
|
|
|
|
lastFullBackupFileName := ""
|
2023-03-17 21:16:04 +05:00
|
|
|
|
err := filepath.WalkDir(filepath.Dir(b.filePath), func(path string, info os.DirEntry, err error) error {
|
2023-03-17 22:17:58 +05:00
|
|
|
|
matched, err := filepath.Match(onlyFullBackupFileMask, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("filepath.WalkDir: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !matched {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastFullBackupFileName = path
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("filepath.WalkDir: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !fullIndex {
|
|
|
|
|
b.logf(LogLevelDebug, "Отсчёт производится от %s.", filepath.Base(lastFullBackupFileName))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var files []string
|
|
|
|
|
err = filepath.WalkDir(filepath.Dir(b.filePath), func(path string, info os.DirEntry, err error) error {
|
|
|
|
|
matched, err := filepath.Match(allFileMask, path)
|
2023-03-11 14:13:35 +05:00
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("filepath.Match: %v", err)
|
|
|
|
|
}
|
2023-03-17 22:17:58 +05:00
|
|
|
|
if matched && path >= lastFullBackupFileName {
|
|
|
|
|
if fullIndex || path >= lastFullBackupFileName {
|
|
|
|
|
files = append(files, path)
|
|
|
|
|
}
|
2023-03-11 14:13:35 +05:00
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("filepath.Walk: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index := &Index{Files: make(map[string]FileHistory)}
|
|
|
|
|
|
|
|
|
|
for i, file := range files {
|
|
|
|
|
if b.logger.MinimalLogLevel <= LogLevelProgress {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\r[%d%%] Чтение файла %s...", (100 * i / len(files)), filepath.Base(file))
|
|
|
|
|
}
|
|
|
|
|
f, err := os.Open(file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("os.Open: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
decoder, err := zstd.NewReader(f)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("zstd.NewReader: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tarReader := tar.NewReader(decoder)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
tarHeader, err := tarReader.Next()
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("ошибка при чтении списка файлов из архива %s: %v", file, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b.logf(LogLevelDebug, "Найден файл %s...\n", tarHeader.Name)
|
|
|
|
|
|
|
|
|
|
index.Files[tarHeader.Name] = append(index.Files[tarHeader.Name], File{
|
|
|
|
|
DestinationPath: tarHeader.Name,
|
|
|
|
|
Info: tarHeader.FileInfo(),
|
|
|
|
|
ArchiveFile: file})
|
|
|
|
|
}
|
|
|
|
|
decoder.Close()
|
|
|
|
|
}
|
|
|
|
|
if b.logger.MinimalLogLevel <= LogLevelProgress && len(files) > 0 {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\r[%d%%] Чтение файлов завершено.\n", 100) // TODO: нужна очистка строки, т.к. данная строка короче имени файлов
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return index, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test осуществляет проверку архивов и возвращает первую встретившуюся ошибку
|
|
|
|
|
func (b *Config) Test() error {
|
2023-03-17 22:17:58 +05:00
|
|
|
|
_, err := b.index(true) // TODO: улучшить реализацию
|
2023-03-11 14:13:35 +05:00
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|