* update logger routines
* add index file support
* split file masks and path masks
This commit is contained in:
nxshock 2023-04-03 19:43:16 +05:00
parent 89c481f95d
commit 19e809966a
16 changed files with 530 additions and 538 deletions

View File

@ -7,166 +7,12 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"github.com/klauspost/compress/zstd"
"github.com/nxshock/progressmessage"
)
type Pattern struct {
Path string
// Маски имени файла
FileNamePatternList []string
// Маски пути
FilePathPatternList []string
// Вкючать файлы в покаталогах
Recursive bool
}
// FindAll возвращает индекс файлов, совпавших по маске
func (b *Config) FindAll(mask string) (*Index, error) {
b.logf(LogLevelDebug, "Поиск маски %s...", mask)
index, err := b.index(true)
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 {
index, err := b.index(false)
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" // Инкрементальный бекап
}
filePath := filepath.Join(filepath.Dir(b.filePath), b.FileName+"_"+time.Now().Local().Format(defaulFileNameTimeFormat)+suffix+defaultExt)
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) {
func (b *Config) fileList(fileNames chan FileInfo) {
errorCount := 0
for _, mask := range b.Patterns {
@ -174,7 +20,7 @@ func (b *Config) fileList(fileNames chan File) {
err := filepath.WalkDir(mask.Path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
errorCount++
b.logf(LogLevelCritical, "Ошибка при поиске файлов: %v\n", err)
b.logf(Error, "Ошибка при поиске файлов: %v\n", err)
if b.StopOnAnyError {
return fmt.Errorf("ошибка при переборе файлов: %v", err)
}
@ -196,36 +42,36 @@ func (b *Config) fileList(fileNames chan File) {
info, err := os.Stat(path)
if err != nil {
errorCount++
b.logf(LogLevelCritical, "Ошибка при получении информации о файле: %v\n", err)
b.logf(Error, "get file info error: %v", err)
if b.StopOnAnyError {
return fmt.Errorf("ошибка при получении информации о файле: %v", err)
return fmt.Errorf("get file info error: %v", err)
}
}
file := File{
SourcePath: path,
DestinationPath: filepath.ToSlash(path),
Info: info}
file := FileInfo{
filePath: path,
ModificationTime: info.ModTime(),
fileSize: info.Size()}
fileNames <- file
}
}
return nil
})
if err != nil {
b.logf(LogLevelCritical, "Ошибка при получении списка файлов: %v\n", err)
b.logf(Error, "get file list error: %v\n", err)
}
} else {
allFilesAndDirs, err := filepath.Glob(filepath.Join(mask.Path, "*"))
if err != nil {
errorCount++
b.logf(LogLevelCritical, "Ошибка при получении списка файлов: %v\n", err)
b.logf(Error, "get file list error: %v\n", err)
}
for _, fileOrDirPath := range allFilesAndDirs {
info, err := os.Stat(fileOrDirPath)
if err != nil {
errorCount++
b.logf(LogLevelCritical, "Ошибка при получении информации об объекте: %v\n", err)
b.logf(Error, "get object info error: %v\n", err)
continue
}
@ -233,15 +79,11 @@ func (b *Config) fileList(fileNames chan File) {
continue
}
//fileName := filepath.Base(fileOrDirPath)
fileName := fileOrDirPath // TODO: тестирование, маска должна накладываться на путь
if isFilePathMatchPatterns(mask.FilePathPatternList, fileName) && isFileNameMatchPatterns(mask.FileNamePatternList, fileName) {
if !isFilePathMatchPatterns(b.GlobalExcludeFilePathPatterns, fileName) && !isFileNameMatchPatterns(b.GlobalExcludeFileNamePatterns, fileName) {
file := File{
SourcePath: fileOrDirPath,
DestinationPath: filepath.ToSlash(fileOrDirPath),
Info: info}
if isFilePathMatchPatterns(mask.FilePathPatternList, fileOrDirPath) && isFileNameMatchPatterns(mask.FileNamePatternList, fileOrDirPath) {
if !isFilePathMatchPatterns(b.GlobalExcludeFilePathPatterns, fileOrDirPath) && !isFileNameMatchPatterns(b.GlobalExcludeFileNamePatterns, fileOrDirPath) {
file := FileInfo{
filePath: fileOrDirPath,
ModificationTime: info.ModTime()}
fileNames <- file
}
}
@ -250,14 +92,131 @@ func (b *Config) fileList(fileNames chan File) {
}
if errorCount > 0 {
b.logf(LogLevelCritical, "Ошибок: %d\n", errorCount)
b.logf(Error, "Ошибок: %d\n", errorCount)
}
close(fileNames)
}
func (b *Config) FullBackup() error {
return b.doBackup(make(Index))
}
func (b *Config) IncrementalBackup() error {
index, err := b.index(false)
if err != nil {
return err
}
return b.doBackup(index)
}
func (b *Config) doBackup(index Index) error {
var suffix string
if len(index) == 0 {
suffix = "f" // Full backup - полный бекап
} else {
suffix = "i" // Инкрементальный бекап
}
filePath := filepath.Join(filepath.Dir(b.filePath), b.FileName+"_"+time.Now().Local().Format(defaulFileNameTimeFormat)+suffix+defaultExt)
var err error
filePath, err = filepath.Abs(filePath)
if err != nil {
return fmt.Errorf("ошибка при создании файла архива: %v", err)
}
b.logf(Info, "Creating new file %s...", filepath.Base(filePath))
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(Info, "Copying files...")
addedFileIndex := make(Index)
i := 0 // processed file count
addSize := int64(0) // added bytes
for k := range b.planChan(index) {
i++
addSize += k.fileSize
err := b.addFileToTarWriter(k.filePath, tarWriter)
if err != nil {
b.logf(Error, "add file error %s: %v\n", k.filePath, err)
if b.StopOnAnyError {
compressor.Close()
resultArchiveFile.Close()
os.Remove(filePath)
return fmt.Errorf("add file error: %v", err) // TODO: организовать закрытие и удаление частичного файла
}
}
addedFileIndex.AddFile(k.filePath, filepath.Base(filePath), k.ModificationTime)
}
err = tarWriter.Close()
if err != nil {
compressor.Close()
resultArchiveFile.Close()
os.Remove(filePath)
return fmt.Errorf("close tar file error: %v", err)
}
err = compressor.Close()
if err != nil {
resultArchiveFile.Close()
os.Remove(filePath)
return fmt.Errorf("close compressor error: %v", err)
}
err = resultArchiveFile.Close()
if err != nil {
return fmt.Errorf("close file error: %v", err)
}
if i == 0 {
b.logf(Info, "No new or updated files found.")
} else if i == 1 {
b.logf(Info, "%d file added, %s.", i, sizeToApproxHuman(addSize))
} else {
b.logf(Info, "%d files added, %s.", i, sizeToApproxHuman(addSize))
}
// если не было обновлений, удалить пустой файл
if i == 0 {
err = os.Remove(filePath)
if err != nil {
return err
}
}
// если были обновления - обновить индексный файл
if i > 0 {
for fileName, fileHistory := range addedFileIndex {
for _, historyItem := range fileHistory {
index.AddFile(fileName, historyItem.ArchiveFileName, historyItem.ModificationTime)
}
}
err = index.Save(filepath.Join(filepath.Dir(b.filePath), indexFileName))
if err != nil {
return err
}
}
return nil
}
func (b *Config) addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
b.logf(LogLevelDebug, "Добавление файла %s...\n", filePath)
b.logf(Debug, "Adding file %s...\n", filePath)
file, err := os.Open(filePath)
if err != nil {
@ -288,147 +247,3 @@ func (b *Config) addFileToTarWriter(filePath string, tarWriter *tar.Writer) erro
return nil
}
// GetFileWithTime возвращает содержимое файла на указанную дату.
func (b *Config) GetFileWithTime(path string, t time.Time, w io.Writer) error {
index, err := b.index(true)
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
}
// fullIndex - true = все файлы, false = только от последнего полного
func (b *Config) index(fullIndex bool) (*Index, error) {
b.logf(LogLevelInfo, "Построение индекса текущего архива из %s...", filepath.Dir(b.filePath))
allFileMask := filepath.Join(filepath.Dir(b.filePath), b.FileName+"*"+defaultExt)
onlyFullBackupFileMask := filepath.Join(filepath.Dir(b.filePath), b.FileName+"*f"+defaultExt)
// Get last full backup name
lastFullBackupFileName := ""
err := filepath.WalkDir(filepath.Dir(b.filePath), func(path string, info os.DirEntry, err error) error {
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)
if err != nil {
return fmt.Errorf("filepath.Match: %v", err)
}
if matched && path >= lastFullBackupFileName {
if fullIndex || path >= lastFullBackupFileName {
files = append(files, path)
}
}
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 {
_, err := b.index(true) // TODO: улучшить реализацию
return err
}

View File

@ -2,18 +2,16 @@ package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/tidwall/match"
"github.com/BurntSushi/toml"
)
type Backuper struct {
Config *Config
}
type Config struct {
// Имя файлов бекапа без расширения
FileName string
@ -27,19 +25,13 @@ type Config struct {
// Маски путей для исключения
GlobalExcludeFilePathPatterns []string
// Логгер
Logger LoggerConfig
logger Logger
// Останавливать обработку при любой ошибке
StopOnAnyError bool
filePath string
}
// Уровень логирования
LogLevel LogLevel
type LoggerConfig struct {
Name string
MinimalLogLevel LogLevel
filePath string
}
func (config *Config) Save(filepath string) error {
@ -71,8 +63,6 @@ func LoadConfig(filePath string) (*Config, error) {
return nil, fmt.Errorf("decode file: %v", err)
}
config.logger = Logger{logger: log.New(os.Stderr, "", 0), MinimalLogLevel: config.Logger.MinimalLogLevel}
for _, mask := range config.Patterns {
if len(mask.FilePathPatternList) == 0 {
mask.FilePathPatternList = []string{"*"}
@ -89,9 +79,9 @@ func LoadConfig(filePath string) (*Config, error) {
}
// planChan возвращает канал, в который засылает список файлов для добавления/обновления
func (b *Config) planChan(index *Index) chan File {
allFilesChan := make(chan File, 64) // TODO: размер очереди?
addFilesChan := make(chan File, 64) // TODO: размер очереди?
func (b *Config) planChan(index Index) chan FileInfo {
allFilesChan := make(chan FileInfo, 64) // TODO: размер очереди?
addFilesChan := make(chan FileInfo, 64) // TODO: размер очереди?
go func() { b.fileList(allFilesChan) }()
@ -103,13 +93,13 @@ func (b *Config) planChan(index *Index) chan File {
continue
}
existingFile, exists := index.Files[file.DestinationPath]
existingFile, exists := index[file.filePath]
if !exists {
addFilesChan <- file
continue
}
if file.Info.ModTime().Truncate(time.Second).After(existingFile.Latest().Info.ModTime().Truncate(time.Second)) {
if file.ModificationTime.Truncate(time.Second).After(existingFile.Latest().ModificationTime.Truncate(time.Second)) {
addFilesChan <- file
continue
}
@ -120,3 +110,23 @@ func (b *Config) planChan(index *Index) chan File {
return addFilesChan
}
// FindAll возвращает индекс файлов, совпавших по маске
func (b *Config) FindAll(pattern string) (Index, error) {
index, err := b.index(true)
if err != nil {
return nil, fmt.Errorf("index: %v", err)
}
result := make(Index)
for path, info := range index {
if match.Match(strings.ToLower(path), pattern) {
for _, historyItem := range info {
result.AddFile(path, historyItem.ArchiveFileName, historyItem.ModificationTime)
}
}
}
return result, nil
}

View File

@ -9,4 +9,7 @@ const (
// Формат времени для файлов
defaulFileNameTimeFormat = "2006-01-02_15-04-05"
//
indexFileName = "index.csv.zst"
)

View File

@ -1,10 +0,0 @@
FileName = "backup-go-projects"
StopOnAnyError = false
[Logger]
MinimalLogLevel = 1
[[Patterns]]
Path = "/home/user/go/src"
FileNamePatternList = ["*.go", "go.mod", "go.sum"]
Recursive = true

View File

@ -4,6 +4,7 @@ import (
"archive/tar"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
@ -13,8 +14,8 @@ import (
type ExtractionPlan map[string][]string // filepath - array of internal paths
func (b *Backuper) extractionPlan(mask string, t time.Time) (ExtractionPlan, error) {
index, err := b.Config.index(true)
func (b *Config) extractionPlan(mask string, t time.Time) (ExtractionPlan, error) {
index, err := b.index(true)
if err != nil {
return nil, fmt.Errorf("extractionPlan: %v", err)
}
@ -27,15 +28,16 @@ func (b *Backuper) extractionPlan(mask string, t time.Time) (ExtractionPlan, err
plan := make(ExtractionPlan)
for _, file := range files {
plan[file.ArchiveFile] = append(plan[file.ArchiveFile], file.DestinationPath)
plan[file.ArchiveFileName] = append(plan[file.ArchiveFileName], file.filePath)
}
return plan, nil
}
func (b *Backuper) extract(extractionPlan ExtractionPlan, toDir string) error {
func (b *Config) extract(extractionPlan ExtractionPlan, toDir string) error {
for archiveFile, files := range extractionPlan {
f, err := os.Open(archiveFile)
log.Printf("Восстановление из архивного файла %s...", filepath.Join(filepath.Dir(b.filePath), archiveFile))
f, err := os.Open(filepath.Join(filepath.Dir(b.filePath), archiveFile))
if err != nil {
return fmt.Errorf("ошибка при чтении файла архива: %v", err)
}
@ -58,6 +60,7 @@ func (b *Backuper) extract(extractionPlan ExtractionPlan, toDir string) error {
return fmt.Errorf("ошибка при чтении tar-содержимого: %v", err)
}
if inArr, i := stringIn(header.Name, files); inArr {
log.Printf("Восстановление файла %s...", header.Name)
resultFilePath := filepath.Join(toDir, clean(header.Name))
os.MkdirAll(filepath.Dir(resultFilePath), 0644)
f, err := os.Create(resultFilePath)

19
file.go
View File

@ -1,19 +0,0 @@
package main
import (
"os"
)
type File struct {
// Исходный путь
SourcePath string
// Путь в архиве
DestinationPath string
// Путь к архиву
ArchiveFile string
// Информация о файле
Info os.FileInfo
}

View File

@ -1,14 +1,14 @@
package main
// FileHistory содержит историю изменения файла
type FileHistory []File
type FileHistory []FileInfo
// Latest возвращает информацию о последней версии файла
func (fileHistory FileHistory) Latest() File {
func (fileHistory FileHistory) Latest() FileInfo {
file := fileHistory[0]
for i := 1; i < len(fileHistory); i++ {
if fileHistory[i].Info.ModTime().After(file.Info.ModTime()) {
if fileHistory[i].ModificationTime.After(file.ModificationTime) {
file = fileHistory[i]
}
}
@ -24,5 +24,5 @@ func (fileHistory FileHistory) Swap(i, j int) {
}
func (fileHistory FileHistory) Less(i, j int) bool {
return fileHistory[i].Info.ModTime().Before(fileHistory[j].Info.ModTime())
return fileHistory[i].ModificationTime.Before(fileHistory[j].ModificationTime)
}

3
go.mod
View File

@ -4,8 +4,7 @@ go 1.20
require (
github.com/BurntSushi/toml v1.2.1
github.com/klauspost/compress v1.16.0
github.com/nxshock/progressmessage v0.0.0-20210730035634-63cec26e1e83
github.com/klauspost/compress v1.16.3
github.com/stretchr/testify v1.8.2
github.com/tidwall/match v1.1.1
)

6
go.sum
View File

@ -3,10 +3,8 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/nxshock/progressmessage v0.0.0-20210730035634-63cec26e1e83 h1:WjqT/HWUQp14LpSodOU0AX1Gd2N2AsasmDQEXQFdhvU=
github.com/nxshock/progressmessage v0.0.0-20210730035634-63cec26e1e83/go.mod h1:QZBXJ8qLaGwgOeBsNLTV9++wQONNSlDz9y55S5UJ+EM=
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

290
index.go
View File

@ -1,84 +1,264 @@
package main
import (
"bytes"
"errors"
"archive/tar"
"encoding/csv"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"time"
"github.com/klauspost/compress/zstd"
)
type Index struct {
Files map[string]FileHistory // Путь -
type FileInfo struct {
ArchiveFileName string
ModificationTime time.Time
filePath string
fileSize int64
}
func (fileHistory FileHistory) String() string {
var b bytes.Buffer
type Index map[string]FileHistory
b.WriteString("[")
for i := 0; i < len(fileHistory); i++ {
if i > 0 {
fmt.Fprintf(&b, ", %s", fileHistory[i].Info.ModTime().Local().Format(defaultTimeFormat))
func (index Index) AddFile(fileName string, archiveFileName string, modTime time.Time) {
fileInfo := FileInfo{ArchiveFileName: archiveFileName, ModificationTime: modTime}
if eFileInfo, exists := index[fileName]; exists {
index[fileName] = append(eFileInfo, fileInfo)
return
}
index[fileName] = FileHistory{fileInfo}
}
func (index Index) ViewFileVersions(w io.Writer) error {
for filePath, fileHistory := range index {
_, err := fmt.Fprintf(w, "%s\n", filePath)
if err != nil {
return err
}
for _, v := range fileHistory {
_, err := fmt.Fprintf(w, "\t%s %s\n", v.ModificationTime.Format(defaultTimeFormat), v.ArchiveFileName)
if err != nil {
return err
}
}
}
return nil
}
func (index Index) Save(fileName string) error {
f, err := os.Create(fileName)
if err != nil {
return err
}
enc, err := zstd.NewWriter(f, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil {
f.Close() // TODO: удалить частичный файл?
return err
}
files := make([]string, 0, len(index))
for fileName := range index {
files = append(files, fileName)
}
// Sort file list for better compression
sort.Strings(files)
csvWriter := csv.NewWriter(enc)
csvWriter.Comma = ';'
for _, fileName := range files {
for _, historyItem := range index[fileName] {
err := csvWriter.Write([]string{fileName, historyItem.ArchiveFileName, strconv.Itoa(int(historyItem.ModificationTime.Unix()))})
if err != nil {
enc.Close()
f.Close() // TODO: удалить частичный файл?
return err
}
}
}
csvWriter.Flush()
if err := csvWriter.Error(); err != nil {
enc.Close()
f.Close() // TODO: удалить частичный файл?
return err
}
err = enc.Close()
if err != nil {
f.Close() // TODO: удалить частичный файл?
return err
}
err = f.Close()
if err != nil {
return err
}
return err
}
func (b *Config) index(fullIndex bool) (Index, error) {
index, err := b.indexFromFile()
if err == nil {
b.logf(Debug, "Index file contains %d of files.", len(index))
return index, nil
}
b.logf(Error, "index file read error: %v", err)
return b.indexFromDisk(fullIndex)
}
func (b *Config) indexFromFile() (Index, error) {
index := make(Index)
indexFileName := filepath.Join(filepath.Dir(b.filePath), indexFileName)
f, err := os.Open(indexFileName)
if err != nil {
return nil, err
}
defer f.Close()
dec, err := zstd.NewReader(f)
if err != nil {
return nil, err
}
defer dec.Close()
csvReader := csv.NewReader(dec)
csvReader.Comma = ';'
csvReader.FieldsPerRecord = 3
for {
data, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
unixTime, err := strconv.Atoi(data[2])
if err != nil {
return nil, err
}
index.AddFile(data[0], data[1], time.Unix(int64(unixTime), 0).Local())
}
return index, nil
}
func (b *Config) indexFromDisk(fullIndex bool) (Index, error) {
b.logf(Info, "Rebuilding index from %s...", filepath.Dir(b.filePath))
allFileMask := filepath.Join(filepath.Dir(b.filePath), b.FileName+"*"+defaultExt)
onlyFullBackupFileMask := filepath.Join(filepath.Dir(b.filePath), b.FileName+"*f"+defaultExt)
// Get last full backup name
lastFullBackupFileName := ""
err := filepath.WalkDir(filepath.Dir(b.filePath), func(path string, info os.DirEntry, err error) error {
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(Debug, "Diff will be calculated from %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)
if err != nil {
return fmt.Errorf("filepath.Match: %v", err)
}
if matched && (fullIndex || path >= lastFullBackupFileName) {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("filepath.Walk: %v", err)
}
index := make(Index)
for i, file := range files {
b.logf(Debug, "[%3d%%] Reading file %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 {
fmt.Fprintf(&b, "%s", fileHistory[i].Info.ModTime().Local().Format(defaultTimeFormat))
return nil, fmt.Errorf("ошибка при чтении списка файлов из архива %s: %v", file, err)
}
}
b.WriteString("]")
return b.String()
index[tarHeader.Name] = append(index[tarHeader.Name], FileInfo{
filePath: tarHeader.Name,
ModificationTime: tarHeader.FileInfo().ModTime(),
fileSize: tarHeader.FileInfo().Size(),
ArchiveFileName: filepath.Base(file)})
}
decoder.Close()
}
return index, nil
}
func (index *Index) ItemCount() int {
return len(index.Files)
}
func (index Index) GetFilesLocation(mask string, t time.Time) ([]FileInfo, error) {
var files2 []FileInfo
func (index *Index) GetFileWithTime(path string, t time.Time) (File, error) {
files, exists := index.Files[path]
if !exists {
return File{}, errors.New("not exists")
}
file := files[0]
for i := 1; i < len(files); i++ {
if files[i].Info.ModTime().Before(t) && files[i].Info.ModTime().Sub(t) > file.Info.ModTime().Sub(t) { // Больше, т.к. отрицательные числа
file = files[i]
}
}
return file, nil
}
func (index *Index) String() string {
var b bytes.Buffer
for path, info := range index.Files {
sort.Sort(info)
fmt.Fprintf(&b, "%s %s\n", path, info)
}
if b.Len() > 0 {
b.Truncate(b.Len() - 1)
}
return b.String()
}
func (index *Index) GetFilesLocation(mask string, t time.Time) ([]File, error) {
var files2 []File
for fileName := range index.Files {
for fileName := range index {
if isFilePathMatchPatterns([]string{mask}, fileName) {
files := index.Files[fileName]
files := index[fileName]
file := files[0]
for i := 1; i < len(files); i++ {
if files[i].Info.ModTime().Before(t) && files[i].Info.ModTime().Sub(t) > file.Info.ModTime().Sub(t) { // Больше, т.к. отрицательные числа
if files[i].ModificationTime.Before(t) && files[i].ModificationTime.Sub(t) > file.ModificationTime.Sub(t) { // Больше, т.к. отрицательные числа
file = files[i]
}
}
file.filePath = fileName
files2 = append(files2, file)
}
}

27
index_test.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestIndexAddFile(t *testing.T) {
index := make(Index)
assert.Len(t, index, 0)
fileName := "file"
archiveFileName := "archive"
modTime := time.Now()
index.AddFile(fileName, archiveFileName, modTime)
assert.Len(t, index, 1)
assert.Len(t, index[fileName], 1)
expectedFileInfo := FileInfo{
ArchiveFileName: archiveFileName,
ModificationTime: modTime}
assert.Equal(t, expectedFileInfo, index[fileName][0])
}

60
log.go
View File

@ -1,60 +0,0 @@
package main
import (
"log"
)
type LogLevel int
const (
LogLevelDebug = iota // 0
LogLevelProgress // 1
LogLevelInfo // 2
LogLevelWarning // 3
LogLevelCritical // 4
)
type Logger struct {
logger *log.Logger
MinimalLogLevel LogLevel
}
func (logger *Logger) log(logLevel LogLevel, a ...interface{}) {
if logLevel < logger.MinimalLogLevel {
return
}
logger.logger.Print(a...)
}
func (logger *Logger) logf(logLevel LogLevel, s string, a ...interface{}) {
if logLevel < logger.MinimalLogLevel {
return
}
logger.logger.Printf(s, a...)
}
func (logger *Logger) logln(logLevel LogLevel, a ...interface{}) {
if logLevel < logger.MinimalLogLevel {
return
}
logger.logger.Println(a...)
}
func (logger *Logger) fatalln(a ...interface{}) {
logger.logger.Fatalln(a...)
}
func (b *Config) log(logLevel LogLevel, a ...interface{}) {
b.logger.log(logLevel, a...)
}
func (b *Config) logf(logLevel LogLevel, s string, a ...interface{}) {
b.logger.logf(logLevel, s, a...)
}
func (b *Config) fatalln(a ...interface{}) {
b.logger.fatalln(a...)
}

32
logger.go Normal file
View File

@ -0,0 +1,32 @@
package main
import "log"
type LogLevel int
const (
Debug LogLevel = iota
Info
Warn
Error
)
func (b *Config) log(l LogLevel, a ...interface{}) {
if l < b.LogLevel {
return
}
log.Print(a...)
}
func (b *Config) logf(l LogLevel, s string, a ...interface{}) {
if l < b.LogLevel {
return
}
log.Printf(s, a...)
}
func (b *Config) fatalln(a ...interface{}) {
log.Fatalln(a...)
}

82
main.go
View File

@ -1,7 +1,6 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
@ -13,22 +12,12 @@ func init() {
}
func main() {
if len(os.Args) <= 1 {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
switch os.Args[1] {
case "i":
config, err := LoadConfig(os.Args[2])
if err != nil {
log.Fatalln("ошибка при чтении конфига:", err)
}
err = config.IncrementalBackup()
if err != nil {
config.fatalln("ошибка инкрементального бекапа:", err)
}
case "f":
config, err := LoadConfig(os.Args[2])
if err != nil {
@ -37,47 +26,62 @@ func main() {
err = config.FullBackup()
if err != nil {
config.fatalln("ошибка полного бекапа:", err)
log.Fatalln(err)
}
case "i":
config, err := LoadConfig(os.Args[2])
if err != nil {
log.Fatalln(err)
}
err = config.IncrementalBackup()
if err != nil {
log.Fatalln(err)
}
case "s":
config, err := LoadConfig(os.Args[2])
if err != nil {
log.Fatalln("ошибка при чтении конфига:", err)
log.Fatalln("read config error:", err)
}
config.logf(LogLevelProgress, "Поиск файлов по маске %s...\n", os.Args[3])
config.logf(LogLevelProgress, "Создание индекса...\n")
idx, err := config.FindAll(os.Args[3])
if err != nil {
config.fatalln("ошибка поиска:", err)
config.fatalln("search error:", err)
}
config.logf(LogLevelProgress, "Создание индекса завершено.\n")
fmt.Println(idx)
idx.ViewFileVersions(os.Stdout)
case "r":
config, err := LoadConfig(os.Args[2])
if err != nil {
log.Fatalln(err)
}
/*idx, err := config.FindAll(os.Args[3])
var t time.Time
switch len(os.Args[4]) {
case len("02.01.2006"):
t, err = time.Parse("02.01.2006 15:04", os.Args[4])
if err != nil {
config.fatalln(err)
}*/
t, err := time.Parse("02.01.2006 15:04", os.Args[4])
config.fatalln("time parse error:", err)
}
case len("02.01.2006 15:04"):
t, err = time.Parse("02.01.2006 15:04", os.Args[4])
if err != nil {
config.fatalln("ошибка парсинга времени:", err)
config.fatalln("time parse error:", err)
}
case len("02.01.2006 15:04:05"):
t, err = time.Parse("02.01.2006 15:04:05", os.Args[4])
if err != nil {
config.fatalln("time parse error:", err)
}
default:
config.fatalln(`wrong time format, must be ["DD.MM.YYYY", "DD.MM.YYYY hh:mm", "DD.MM.YYYY hh:mm:ss"]`)
}
//
b := &Backuper{Config: config}
plan, err := b.extractionPlan(os.Args[3], t)
plan, err := config.extractionPlan(os.Args[3], t)
if err != nil {
log.Fatalln(err)
}
err = b.extract(plan, os.Args[5])
err = config.extract(plan, os.Args[5])
if err != nil {
log.Fatalln(err)
}
@ -86,11 +90,11 @@ func main() {
if err != nil {
log.Fatalln(err)
}
err = config.Test()
_, err = config.index(true)
if err != nil {
log.Fatalln("ошибка тестирования:", err)
log.Fatalln(err)
}
log.Println("Ошибок нет.")
default:
printUsage()
}
@ -99,10 +103,10 @@ func main() {
func printUsage() {
bin := filepath.Base(os.Args[0])
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, "%s i <config file path> - do incremental backup\n", bin)
fmt.Fprintf(os.Stderr, "%s f <config file path> - do full backup\n", bin)
fmt.Fprintf(os.Stderr, "%s s <config file path> <mask> - search file(s) in backup\n", bin)
fmt.Fprintf(os.Stderr, "%s r <config file path> <mask> <dd.mm.yyyy hh:mm> <path> - recover file(s) from backup\n", bin)
fmt.Fprintf(os.Stderr, "%s t <config file path> - test archive for errors\n", bin)
log.Print("Usage:\n")
log.Printf("%s i <config file path> - do incremental backup\n", bin)
log.Printf("%s f <config file path> - do full backup\n", bin)
log.Printf("%s s <config file path> <mask> - search file(s) in backup\n", bin)
log.Printf("%s r <config file path> <mask> <dd.mm.yyyy hh:mm> <path> - recover file(s) from backup\n", bin)
log.Printf("%s t <config file path> - test archive for errors\n", bin)
}

15
pattern.go Normal file
View File

@ -0,0 +1,15 @@
package main
type Pattern struct {
// Root directory
Path string
// List of file name patterns
FileNamePatternList []string
// List of file path patterns
FilePathPatternList []string
// Recursive search
Recursive bool
}

View File

@ -8,11 +8,6 @@ import (
"github.com/tidwall/match"
)
/*func winPathToRelative(s string) string {
ss := strings.Split(s, string(os.PathSeparator))
return filepath.Join(ss[1:]...)
}*/
func sizeToApproxHuman(s int64) string {
t := []struct {
Name string