mirror of
https://github.com/nxshock/backuper.git
synced 2024-11-28 00:21:02 +05:00
Initial commit
This commit is contained in:
commit
1f0f94e103
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 nxshock
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
415
backuper.go
Normal file
415
backuper.go
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
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)
|
||||||
|
index, err := b.index()
|
||||||
|
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()
|
||||||
|
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("2006-01-02_15-04-05")+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
|
||||||
|
|
||||||
|
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 {
|
||||||
|
index, err := b.index()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Config) index() (*Index, error) {
|
||||||
|
b.logf(LogLevelInfo, "Построение индекса текущего архива из %s...", filepath.Dir(b.filePath))
|
||||||
|
fileMask := filepath.Join(filepath.Dir(b.filePath), b.FileName+"*"+defaultExt)
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
err := filepath.Walk(filepath.Dir(b.filePath), func(path string, info os.FileInfo, err error) error {
|
||||||
|
matched, err := filepath.Match(fileMask, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filepath.Match: %v", err)
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
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() // TODO: улучшить реализацию
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
113
config.go
Normal file
113
config.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Backuper struct {
|
||||||
|
Config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Имя файлов бекапа без расширения
|
||||||
|
FileName string
|
||||||
|
|
||||||
|
// Маски файлов для включения в архив
|
||||||
|
Masks []Mask
|
||||||
|
|
||||||
|
// Маски файлов/путей для исключения из всех масок
|
||||||
|
GlobalExcludeMasks []string
|
||||||
|
|
||||||
|
// Логгер
|
||||||
|
Logger LoggerConfig
|
||||||
|
logger Logger
|
||||||
|
|
||||||
|
// Останавливать обработку при любой ошибке
|
||||||
|
StopOnAnyError bool
|
||||||
|
|
||||||
|
filePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggerConfig struct {
|
||||||
|
Name string
|
||||||
|
MinimalLogLevel LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) Save(filepath string) error {
|
||||||
|
f, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = toml.NewEncoder(f).Encode(config)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(filePath string) (*Config, error) {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open file: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
_, err = toml.DecodeReader(f, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.logger = Logger{logger: log.New(os.Stderr, "", 0), MinimalLogLevel: config.Logger.MinimalLogLevel}
|
||||||
|
|
||||||
|
configFilePath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.filePath = configFilePath
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// planChan возвращает канал, в который засылает список файлов для добавления/обновления
|
||||||
|
func (b *Config) planChan(index *Index) chan File {
|
||||||
|
allFilesChan := make(chan File, 64) // TODO: размер очереди?
|
||||||
|
addFilesChan := make(chan File, 64) // TODO: размер очереди?
|
||||||
|
|
||||||
|
go func() { b.fileList(allFilesChan) }()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for file := range allFilesChan {
|
||||||
|
// Если индекса нет, добавляются все файлы
|
||||||
|
if index == nil {
|
||||||
|
addFilesChan <- file
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
existingFile, exists := index.Files[file.DestinationPath]
|
||||||
|
if !exists {
|
||||||
|
addFilesChan <- file
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.Info.ModTime().Truncate(time.Second).After(existingFile.Latest().Info.ModTime().Truncate(time.Second)) {
|
||||||
|
addFilesChan <- file
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
close(addFilesChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return addFilesChan
|
||||||
|
}
|
9
consts.go
Normal file
9
consts.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Расширение для файлов архивов
|
||||||
|
defaultExt = ".tar.zst"
|
||||||
|
|
||||||
|
// Формат времени для сообщений
|
||||||
|
defaultTimeFormat = "02.01.06 15:04"
|
||||||
|
)
|
10
examples/config.toml
Normal file
10
examples/config.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FileName = "backup-go-projects"
|
||||||
|
StopOnAnyError = false
|
||||||
|
|
||||||
|
[Logger]
|
||||||
|
MinimalLogLevel = 1
|
||||||
|
|
||||||
|
[[Masks]]
|
||||||
|
Path = "/home/user/go/src"
|
||||||
|
MaskList = ["*.go", "*/go.mod", "*/go.sum"]
|
||||||
|
Recursive = true
|
85
extractionplan.go
Normal file
85
extractionplan.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("extractionPlan: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := index.GetFilesLocation(mask, t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("extractionPlan: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := make(ExtractionPlan)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
plan[file.ArchiveFile] = append(plan[file.ArchiveFile], file.DestinationPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Backuper) extract(extractionPlan ExtractionPlan, toDir string) error {
|
||||||
|
for archiveFile, files := range extractionPlan {
|
||||||
|
f, err := os.Open(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 inArr, i := stringIn(header.Name, files); inArr {
|
||||||
|
resultFilePath := filepath.Join(toDir, clean(header.Name))
|
||||||
|
os.MkdirAll(filepath.Dir(resultFilePath), 0644)
|
||||||
|
f, err := os.Create(resultFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(f, tarReader)
|
||||||
|
if err != nil {
|
||||||
|
f.Close() // TODO: удалять частичный файл?
|
||||||
|
return fmt.Errorf("ошибка при извлечении файла из tar-архива: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
files[i] = files[len(files)-1]
|
||||||
|
files = files[:len(files)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
19
file.go
Normal file
19
file.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
// Исходный путь
|
||||||
|
SourcePath string
|
||||||
|
|
||||||
|
// Путь в архиве
|
||||||
|
DestinationPath string
|
||||||
|
|
||||||
|
// Путь к архиву
|
||||||
|
ArchiveFile string
|
||||||
|
|
||||||
|
// Информация о файле
|
||||||
|
Info os.FileInfo
|
||||||
|
}
|
28
filehistory.go
Normal file
28
filehistory.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// FileHistory содержит историю изменения файла
|
||||||
|
type FileHistory []File
|
||||||
|
|
||||||
|
// Latest возвращает информацию о последней версии файла
|
||||||
|
func (fileHistory FileHistory) Latest() File {
|
||||||
|
file := fileHistory[0]
|
||||||
|
|
||||||
|
for i := 1; i < len(fileHistory); i++ {
|
||||||
|
if fileHistory[i].Info.ModTime().After(file.Info.ModTime()) {
|
||||||
|
file = fileHistory[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileHistory FileHistory) Len() int {
|
||||||
|
return len(fileHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileHistory FileHistory) Swap(i, j int) {
|
||||||
|
fileHistory[i], fileHistory[j] = fileHistory[j], fileHistory[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileHistory FileHistory) Less(i, j int) bool {
|
||||||
|
return fileHistory[i].Info.ModTime().Before(fileHistory[j].Info.ModTime())
|
||||||
|
}
|
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module github.com/nxshock/backuper
|
||||||
|
|
||||||
|
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/stretchr/testify v1.8.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
23
go.sum
Normal file
23
go.sum
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
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/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=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
87
index.go
Normal file
87
index.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Index struct {
|
||||||
|
Files map[string]FileHistory // Путь -
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileHistory FileHistory) String() string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
b.WriteString("[")
|
||||||
|
for i := 0; i < len(fileHistory); i++ {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Fprintf(&b, ", %s", fileHistory[i].Info.ModTime().Local().Format(defaultTimeFormat))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%s", fileHistory[i].Info.ModTime().Local().Format(defaultTimeFormat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("]")
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *Index) ItemCount() int {
|
||||||
|
return len(index.Files)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if isFileMatchMasks([]string{mask}, fileName) {
|
||||||
|
files := index.Files[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) { // Больше, т.к. отрицательные числа
|
||||||
|
file = files[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files2 = append(files2, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files2, nil
|
||||||
|
}
|
60
log.go
Normal file
60
log.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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...)
|
||||||
|
}
|
108
main.go
Normal file
108
main.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) <= 1 {
|
||||||
|
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 {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = config.FullBackup()
|
||||||
|
if err != nil {
|
||||||
|
config.fatalln("ошибка полного бекапа:", err)
|
||||||
|
}
|
||||||
|
case "s":
|
||||||
|
config, err := LoadConfig(os.Args[2])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("ошибка при чтении конфига:", 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.logf(LogLevelProgress, "Создание индекса завершено.\n")
|
||||||
|
|
||||||
|
fmt.Println(idx)
|
||||||
|
case "r":
|
||||||
|
config, err := LoadConfig(os.Args[2])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*idx, err := config.FindAll(os.Args[3])
|
||||||
|
if err != nil {
|
||||||
|
config.fatalln(err)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
t, err := time.Parse("02.01.2006 15:04", os.Args[4])
|
||||||
|
if err != nil {
|
||||||
|
config.fatalln("ошибка парсинга времени:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
b := &Backuper{Config: config}
|
||||||
|
plan, err := b.extractionPlan(os.Args[3], t)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
err = b.extract(plan, os.Args[5])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
case "t":
|
||||||
|
config, err := LoadConfig(os.Args[2])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
err = config.Test()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("ошибка тестирования:", err)
|
||||||
|
}
|
||||||
|
log.Println("Ошибок нет.")
|
||||||
|
default:
|
||||||
|
printUsage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
4
todo.go
Normal file
4
todo.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// TODO: Инкрементальный бекап должен быть вычисен от последнего полного бекапа, а не от всех предыдущих бекапов
|
||||||
|
// TODO: Проверить ситуацию, может ли быть в tar-архиве два файла с одним именем
|
55
utils.go
Normal file
55
utils.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*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
|
||||||
|
Val int64
|
||||||
|
}{
|
||||||
|
{"EiB", 1 << 60},
|
||||||
|
{"PiB", 1 << 50},
|
||||||
|
{"TiB", 1 << 40},
|
||||||
|
{"GiB", 1 << 30},
|
||||||
|
{"MiB", 1 << 20},
|
||||||
|
{"KiB", 1 << 10}}
|
||||||
|
|
||||||
|
for i := 0; i < len(t); i++ {
|
||||||
|
v := float64(s) / float64(t[i].Val)
|
||||||
|
if v < 1.0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%.1f %s", v, t[i].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%d B", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean убирает невозможнын комбинации символов из пути
|
||||||
|
func clean(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, ":", "")
|
||||||
|
s = strings.ReplaceAll(s, `\\`, `\`)
|
||||||
|
s = strings.ReplaceAll(s, `//`, `/`)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringIn - аналог оператора in
|
||||||
|
func stringIn(s string, ss []string) (bool, int) {
|
||||||
|
for i, v := range ss {
|
||||||
|
if v == s {
|
||||||
|
return true, i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, -1
|
||||||
|
}
|
12
utils_test.go
Normal file
12
utils_test.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSizeToApproxHuman(t *testing.T) {
|
||||||
|
assert.Equal(t, "1.0 KiB", sizeToApproxHuman(1024))
|
||||||
|
assert.Equal(t, "1.1 KiB", sizeToApproxHuman(1126))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user