mirror of
https://github.com/nxshock/backuper.git
synced 2024-11-28 00:21:02 +05:00
Rework
* update logger routines * add index file support * split file masks and path masks
This commit is contained in:
parent
89c481f95d
commit
19e809966a
455
backuper.go
455
backuper.go
@ -7,166 +7,12 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/nxshock/progressmessage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Pattern struct {
|
func (b *Config) fileList(fileNames chan FileInfo) {
|
||||||
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) {
|
|
||||||
errorCount := 0
|
errorCount := 0
|
||||||
|
|
||||||
for _, mask := range b.Patterns {
|
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 {
|
err := filepath.WalkDir(mask.Path, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorCount++
|
errorCount++
|
||||||
b.logf(LogLevelCritical, "Ошибка при поиске файлов: %v\n", err)
|
b.logf(Error, "Ошибка при поиске файлов: %v\n", err)
|
||||||
if b.StopOnAnyError {
|
if b.StopOnAnyError {
|
||||||
return fmt.Errorf("ошибка при переборе файлов: %v", err)
|
return fmt.Errorf("ошибка при переборе файлов: %v", err)
|
||||||
}
|
}
|
||||||
@ -196,36 +42,36 @@ func (b *Config) fileList(fileNames chan File) {
|
|||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorCount++
|
errorCount++
|
||||||
b.logf(LogLevelCritical, "Ошибка при получении информации о файле: %v\n", err)
|
b.logf(Error, "get file info error: %v", err)
|
||||||
if b.StopOnAnyError {
|
if b.StopOnAnyError {
|
||||||
return fmt.Errorf("ошибка при получении информации о файле: %v", err)
|
return fmt.Errorf("get file info error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file := File{
|
file := FileInfo{
|
||||||
SourcePath: path,
|
filePath: path,
|
||||||
DestinationPath: filepath.ToSlash(path),
|
ModificationTime: info.ModTime(),
|
||||||
Info: info}
|
fileSize: info.Size()}
|
||||||
fileNames <- file
|
fileNames <- file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.logf(LogLevelCritical, "Ошибка при получении списка файлов: %v\n", err)
|
b.logf(Error, "get file list error: %v\n", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allFilesAndDirs, err := filepath.Glob(filepath.Join(mask.Path, "*"))
|
allFilesAndDirs, err := filepath.Glob(filepath.Join(mask.Path, "*"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorCount++
|
errorCount++
|
||||||
b.logf(LogLevelCritical, "Ошибка при получении списка файлов: %v\n", err)
|
b.logf(Error, "get file list error: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fileOrDirPath := range allFilesAndDirs {
|
for _, fileOrDirPath := range allFilesAndDirs {
|
||||||
info, err := os.Stat(fileOrDirPath)
|
info, err := os.Stat(fileOrDirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorCount++
|
errorCount++
|
||||||
b.logf(LogLevelCritical, "Ошибка при получении информации об объекте: %v\n", err)
|
b.logf(Error, "get object info error: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,15 +79,11 @@ func (b *Config) fileList(fileNames chan File) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
//fileName := filepath.Base(fileOrDirPath)
|
if isFilePathMatchPatterns(mask.FilePathPatternList, fileOrDirPath) && isFileNameMatchPatterns(mask.FileNamePatternList, fileOrDirPath) {
|
||||||
fileName := fileOrDirPath // TODO: тестирование, маска должна накладываться на путь
|
if !isFilePathMatchPatterns(b.GlobalExcludeFilePathPatterns, fileOrDirPath) && !isFileNameMatchPatterns(b.GlobalExcludeFileNamePatterns, fileOrDirPath) {
|
||||||
|
file := FileInfo{
|
||||||
if isFilePathMatchPatterns(mask.FilePathPatternList, fileName) && isFileNameMatchPatterns(mask.FileNamePatternList, fileName) {
|
filePath: fileOrDirPath,
|
||||||
if !isFilePathMatchPatterns(b.GlobalExcludeFilePathPatterns, fileName) && !isFileNameMatchPatterns(b.GlobalExcludeFileNamePatterns, fileName) {
|
ModificationTime: info.ModTime()}
|
||||||
file := File{
|
|
||||||
SourcePath: fileOrDirPath,
|
|
||||||
DestinationPath: filepath.ToSlash(fileOrDirPath),
|
|
||||||
Info: info}
|
|
||||||
fileNames <- file
|
fileNames <- file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,14 +92,131 @@ func (b *Config) fileList(fileNames chan File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if errorCount > 0 {
|
if errorCount > 0 {
|
||||||
b.logf(LogLevelCritical, "Ошибок: %d\n", errorCount)
|
b.logf(Error, "Ошибок: %d\n", errorCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
close(fileNames)
|
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 {
|
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)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -288,147 +247,3 @@ func (b *Config) addFileToTarWriter(filePath string, tarWriter *tar.Writer) erro
|
|||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
52
config.go
52
config.go
@ -2,18 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/match"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Backuper struct {
|
|
||||||
Config *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Имя файлов бекапа без расширения
|
// Имя файлов бекапа без расширения
|
||||||
FileName string
|
FileName string
|
||||||
@ -27,19 +25,13 @@ type Config struct {
|
|||||||
// Маски путей для исключения
|
// Маски путей для исключения
|
||||||
GlobalExcludeFilePathPatterns []string
|
GlobalExcludeFilePathPatterns []string
|
||||||
|
|
||||||
// Логгер
|
|
||||||
Logger LoggerConfig
|
|
||||||
logger Logger
|
|
||||||
|
|
||||||
// Останавливать обработку при любой ошибке
|
// Останавливать обработку при любой ошибке
|
||||||
StopOnAnyError bool
|
StopOnAnyError bool
|
||||||
|
|
||||||
filePath string
|
// Уровень логирования
|
||||||
}
|
LogLevel LogLevel
|
||||||
|
|
||||||
type LoggerConfig struct {
|
filePath string
|
||||||
Name string
|
|
||||||
MinimalLogLevel LogLevel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) Save(filepath string) error {
|
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)
|
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 {
|
for _, mask := range config.Patterns {
|
||||||
if len(mask.FilePathPatternList) == 0 {
|
if len(mask.FilePathPatternList) == 0 {
|
||||||
mask.FilePathPatternList = []string{"*"}
|
mask.FilePathPatternList = []string{"*"}
|
||||||
@ -89,9 +79,9 @@ func LoadConfig(filePath string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// planChan возвращает канал, в который засылает список файлов для добавления/обновления
|
// planChan возвращает канал, в который засылает список файлов для добавления/обновления
|
||||||
func (b *Config) planChan(index *Index) chan File {
|
func (b *Config) planChan(index Index) chan FileInfo {
|
||||||
allFilesChan := make(chan File, 64) // TODO: размер очереди?
|
allFilesChan := make(chan FileInfo, 64) // TODO: размер очереди?
|
||||||
addFilesChan := make(chan File, 64) // TODO: размер очереди?
|
addFilesChan := make(chan FileInfo, 64) // TODO: размер очереди?
|
||||||
|
|
||||||
go func() { b.fileList(allFilesChan) }()
|
go func() { b.fileList(allFilesChan) }()
|
||||||
|
|
||||||
@ -103,13 +93,13 @@ func (b *Config) planChan(index *Index) chan File {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
existingFile, exists := index.Files[file.DestinationPath]
|
existingFile, exists := index[file.filePath]
|
||||||
if !exists {
|
if !exists {
|
||||||
addFilesChan <- file
|
addFilesChan <- file
|
||||||
continue
|
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
|
addFilesChan <- file
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -120,3 +110,23 @@ func (b *Config) planChan(index *Index) chan File {
|
|||||||
|
|
||||||
return addFilesChan
|
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
|
||||||
|
}
|
||||||
|
@ -9,4 +9,7 @@ const (
|
|||||||
|
|
||||||
// Формат времени для файлов
|
// Формат времени для файлов
|
||||||
defaulFileNameTimeFormat = "2006-01-02_15-04-05"
|
defaulFileNameTimeFormat = "2006-01-02_15-04-05"
|
||||||
|
|
||||||
|
//
|
||||||
|
indexFileName = "index.csv.zst"
|
||||||
)
|
)
|
||||||
|
@ -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
|
|
@ -4,6 +4,7 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@ -13,8 +14,8 @@ import (
|
|||||||
|
|
||||||
type ExtractionPlan map[string][]string // filepath - array of internal paths
|
type ExtractionPlan map[string][]string // filepath - array of internal paths
|
||||||
|
|
||||||
func (b *Backuper) extractionPlan(mask string, t time.Time) (ExtractionPlan, error) {
|
func (b *Config) extractionPlan(mask string, t time.Time) (ExtractionPlan, error) {
|
||||||
index, err := b.Config.index(true)
|
index, err := b.index(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("extractionPlan: %v", err)
|
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)
|
plan := make(ExtractionPlan)
|
||||||
|
|
||||||
for _, file := range files {
|
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
|
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 {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка при чтении файла архива: %v", err)
|
return fmt.Errorf("ошибка при чтении файла архива: %v", err)
|
||||||
}
|
}
|
||||||
@ -58,6 +60,7 @@ func (b *Backuper) extract(extractionPlan ExtractionPlan, toDir string) error {
|
|||||||
return fmt.Errorf("ошибка при чтении tar-содержимого: %v", err)
|
return fmt.Errorf("ошибка при чтении tar-содержимого: %v", err)
|
||||||
}
|
}
|
||||||
if inArr, i := stringIn(header.Name, files); inArr {
|
if inArr, i := stringIn(header.Name, files); inArr {
|
||||||
|
log.Printf("Восстановление файла %s...", header.Name)
|
||||||
resultFilePath := filepath.Join(toDir, clean(header.Name))
|
resultFilePath := filepath.Join(toDir, clean(header.Name))
|
||||||
os.MkdirAll(filepath.Dir(resultFilePath), 0644)
|
os.MkdirAll(filepath.Dir(resultFilePath), 0644)
|
||||||
f, err := os.Create(resultFilePath)
|
f, err := os.Create(resultFilePath)
|
||||||
|
19
file.go
19
file.go
@ -1,19 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type File struct {
|
|
||||||
// Исходный путь
|
|
||||||
SourcePath string
|
|
||||||
|
|
||||||
// Путь в архиве
|
|
||||||
DestinationPath string
|
|
||||||
|
|
||||||
// Путь к архиву
|
|
||||||
ArchiveFile string
|
|
||||||
|
|
||||||
// Информация о файле
|
|
||||||
Info os.FileInfo
|
|
||||||
}
|
|
@ -1,14 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
// FileHistory содержит историю изменения файла
|
// FileHistory содержит историю изменения файла
|
||||||
type FileHistory []File
|
type FileHistory []FileInfo
|
||||||
|
|
||||||
// Latest возвращает информацию о последней версии файла
|
// Latest возвращает информацию о последней версии файла
|
||||||
func (fileHistory FileHistory) Latest() File {
|
func (fileHistory FileHistory) Latest() FileInfo {
|
||||||
file := fileHistory[0]
|
file := fileHistory[0]
|
||||||
|
|
||||||
for i := 1; i < len(fileHistory); i++ {
|
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]
|
file = fileHistory[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,5 +24,5 @@ func (fileHistory FileHistory) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fileHistory FileHistory) Less(i, j int) bool {
|
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
3
go.mod
@ -4,8 +4,7 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.2.1
|
github.com/BurntSushi/toml v1.2.1
|
||||||
github.com/klauspost/compress v1.16.0
|
github.com/klauspost/compress v1.16.3
|
||||||
github.com/nxshock/progressmessage v0.0.0-20210730035634-63cec26e1e83
|
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/tidwall/match v1.1.1
|
github.com/tidwall/match v1.1.1
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
||||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.3/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
290
index.go
290
index.go
@ -1,84 +1,264 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"archive/tar"
|
||||||
"errors"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Index struct {
|
type FileInfo struct {
|
||||||
Files map[string]FileHistory // Путь -
|
ArchiveFileName string
|
||||||
|
ModificationTime time.Time
|
||||||
|
|
||||||
|
filePath string
|
||||||
|
fileSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fileHistory FileHistory) String() string {
|
type Index map[string]FileHistory
|
||||||
var b bytes.Buffer
|
|
||||||
|
|
||||||
b.WriteString("[")
|
func (index Index) AddFile(fileName string, archiveFileName string, modTime time.Time) {
|
||||||
for i := 0; i < len(fileHistory); i++ {
|
fileInfo := FileInfo{ArchiveFileName: archiveFileName, ModificationTime: modTime}
|
||||||
if i > 0 {
|
|
||||||
fmt.Fprintf(&b, ", %s", fileHistory[i].Info.ModTime().Local().Format(defaultTimeFormat))
|
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 {
|
} 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 {
|
func (index Index) GetFilesLocation(mask string, t time.Time) ([]FileInfo, error) {
|
||||||
return len(index.Files)
|
var files2 []FileInfo
|
||||||
}
|
|
||||||
|
|
||||||
func (index *Index) GetFileWithTime(path string, t time.Time) (File, error) {
|
for fileName := range index {
|
||||||
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 {
|
|
||||||
if isFilePathMatchPatterns([]string{mask}, fileName) {
|
if isFilePathMatchPatterns([]string{mask}, fileName) {
|
||||||
files := index.Files[fileName]
|
files := index[fileName]
|
||||||
|
|
||||||
file := files[0]
|
file := files[0]
|
||||||
for i := 1; i < len(files); i++ {
|
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 = files[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file.filePath = fileName
|
||||||
|
|
||||||
files2 = append(files2, file)
|
files2 = append(files2, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
index_test.go
Normal file
27
index_test.go
Normal 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
60
log.go
@ -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
32
logger.go
Normal 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
82
main.go
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -13,22 +12,12 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) <= 1 {
|
if len(os.Args) < 2 {
|
||||||
printUsage()
|
printUsage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch os.Args[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":
|
case "f":
|
||||||
config, err := LoadConfig(os.Args[2])
|
config, err := LoadConfig(os.Args[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -37,47 +26,62 @@ func main() {
|
|||||||
|
|
||||||
err = config.FullBackup()
|
err = config.FullBackup()
|
||||||
if err != nil {
|
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":
|
case "s":
|
||||||
config, err := LoadConfig(os.Args[2])
|
config, err := LoadConfig(os.Args[2])
|
||||||
if err != nil {
|
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])
|
idx, err := config.FindAll(os.Args[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
config.fatalln("ошибка поиска:", err)
|
config.fatalln("search error:", err)
|
||||||
}
|
}
|
||||||
config.logf(LogLevelProgress, "Создание индекса завершено.\n")
|
|
||||||
|
|
||||||
fmt.Println(idx)
|
idx.ViewFileVersions(os.Stdout)
|
||||||
case "r":
|
case "r":
|
||||||
config, err := LoadConfig(os.Args[2])
|
config, err := LoadConfig(os.Args[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
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 {
|
if err != nil {
|
||||||
config.fatalln(err)
|
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])
|
t, err = time.Parse("02.01.2006 15:04", os.Args[4])
|
||||||
if err != nil {
|
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"]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
plan, err := config.extractionPlan(os.Args[3], t)
|
||||||
|
|
||||||
b := &Backuper{Config: config}
|
|
||||||
plan, err := b.extractionPlan(os.Args[3], t)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
err = b.extract(plan, os.Args[5])
|
err = config.extract(plan, os.Args[5])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
@ -86,11 +90,11 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
err = config.Test()
|
|
||||||
|
_, err = config.index(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("ошибка тестирования:", err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
log.Println("Ошибок нет.")
|
|
||||||
default:
|
default:
|
||||||
printUsage()
|
printUsage()
|
||||||
}
|
}
|
||||||
@ -99,10 +103,10 @@ func main() {
|
|||||||
func printUsage() {
|
func printUsage() {
|
||||||
bin := filepath.Base(os.Args[0])
|
bin := filepath.Base(os.Args[0])
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
log.Print("Usage:\n")
|
||||||
fmt.Fprintf(os.Stderr, "%s i <config file path> - do incremental backup\n", bin)
|
log.Printf("%s i <config file path> - do incremental backup\n", bin)
|
||||||
fmt.Fprintf(os.Stderr, "%s f <config file path> - do full backup\n", bin)
|
log.Printf("%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)
|
log.Printf("%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)
|
log.Printf("%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.Printf("%s t <config file path> - test archive for errors\n", bin)
|
||||||
}
|
}
|
||||||
|
15
pattern.go
Normal file
15
pattern.go
Normal 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
|
||||||
|
}
|
5
utils.go
5
utils.go
@ -8,11 +8,6 @@ import (
|
|||||||
"github.com/tidwall/match"
|
"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 {
|
func sizeToApproxHuman(s int64) string {
|
||||||
t := []struct {
|
t := []struct {
|
||||||
Name string
|
Name string
|
||||||
|
Loading…
Reference in New Issue
Block a user