Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ The name of your backup is the key at root level. Starting from there you can co
| archive_type | string | No | `tar.gz` | The type of archive to be used (`tar.gz` or `zip` are valid options) |
| add_subfolder | boolean | No | `false` | Creates a new subfolder in `<destination>` for this unit if set to true |
| enabled | boolean | No | `true` | Switch to disable each unit individually |
| use_absolute_paths | boolean | No | `true` | Uses absolute file paths in the archive (see [#11](https://github.com/d-Rickyy-b/backmeup/issues/11)) |

Be careful when using quotes in paths. For most strings you don't even need to use quotes at all. When using double quotes (`"`), you must escape backslashes (`\`) when you want to use them as literal characters (such as in Windows paths).
Check [this handy article](https://www.yaml.info/learn/quote.html) for learning more about quotes in yaml.
141 changes: 93 additions & 48 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,38 @@ var VERBOSE bool
var DEBUG bool

type Unit struct {
name string
sources []string
destination string
excludes []string
archiveType string
addSubfolder bool
enabled bool
name string
sources []string
destination string
excludes []string
archiveType string
addSubfolder bool
enabled bool
useAbsolutePaths bool
}

type Config struct {
units []Unit
}

type BackupFileMetadata struct {
path string
backupBasePath string
}

func (config Config) FromYaml(yamlData []byte) (Config, error) {
// Create a config object from yaml byte array
log.Println("Parsing config yaml")

// Helper struct for parsing the yaml
type YamlUnit struct {
Sources *[]string `yaml:"sources"`
Destination *string `yaml:"destination"`
Excludes *[]string `yaml:"excludes"`
ArchiveType *string `yaml:"archive_type"`
AddSubfolder *bool `yaml:"add_subfolder"`
Enabled *bool `yaml:"enabled"`
Sources *[]string `yaml:"sources"`
Destination *string `yaml:"destination"`
Excludes *[]string `yaml:"excludes"`
ArchiveType *string `yaml:"archive_type"`
AddSubfolder *bool `yaml:"add_subfolder"`
Enabled *bool `yaml:"enabled"`
UseAbsolutePaths *bool `yaml:"use_absolute_paths"`
}

// Sample yaml config:
Expand Down Expand Up @@ -97,6 +104,11 @@ func (config Config) FromYaml(yamlData []byte) (Config, error) {
unit.excludes = *yamlUnit.Excludes
}

unit.useAbsolutePaths = true
if yamlUnit.UseAbsolutePaths != nil {
unit.useAbsolutePaths = *yamlUnit.UseAbsolutePaths
}

if yamlUnit.Sources == nil || yamlUnit.Destination == nil {
log.Fatalf("Sources or destination can't be parsed for unit '%s'", unitName)
} else {
Expand Down Expand Up @@ -147,9 +159,9 @@ func handleExcludes(path string, excludes []string) bool {
return false
}

func getFiles(sourcePath string, excludes []string) ([]string, error) {
func getFiles(sourcePath string, excludes []string) ([]BackupFileMetadata, error) {
// Returns all file paths recursively within a certain source directory
var pathsToBackup []string
var pathsToBackup []BackupFileMetadata

_, statErr := os.Stat(sourcePath)
if statErr != nil {
Expand All @@ -174,7 +186,11 @@ func getFiles(sourcePath string, excludes []string) ([]string, error) {
return nil
}

pathsToBackup = append(pathsToBackup, path)
fileMetadata := BackupFileMetadata{
path: path,
backupBasePath: sourcePath,
}
pathsToBackup = append(pathsToBackup, fileMetadata)
return nil
})
if err != nil {
Expand All @@ -200,50 +216,86 @@ func validatePath(path string, mustBeDir bool) bool {
return true
}

func writeTar(backupArchivePath string, filesToBackup []string) {
file, err := os.Create(backupArchivePath)
func getPathInArchive(filePath string, backupBasePath string, unit Unit) string {
// Remove the base path from the file path within the archive, if option is set
pathInArchive := filePath
if !unit.useAbsolutePaths {
parentBasePath := filepath.Dir(backupBasePath)
pathInArchive = strings.ReplaceAll(filePath, parentBasePath, "")

if strings.HasPrefix(pathInArchive, "\\") {
pathInArchive = pathInArchive[1:]
}
}
return pathInArchive
}

func writeArchive(backupArchivePath string, filesToBackup []BackupFileMetadata, unit Unit) {
archiveFile, err := os.Create(backupArchivePath)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
// set up the gzip writer
gw := gzip.NewWriter(file)
defer archiveFile.Close()

switch unit.archiveType {
case "tar.gz":
writeTar(archiveFile, filesToBackup, unit)
case "zip":
writeZip(archiveFile, filesToBackup, unit)
default:
log.Fatalf("Can't handle archive type '%s'", unit.archiveType)
}
}

func writeTar(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit Unit) {
// set up the gzip and tar writer
gw := gzip.NewWriter(archiveFile)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()

// Init progress bar
bar := pb.StartNew(len(filesToBackup))
for i := range filesToBackup {
if err := addFileToTar(tw, filesToBackup[i]); err != nil {
fileMetadata := filesToBackup[i]
filePath := fileMetadata.path

pathInArchive := getPathInArchive(filePath, fileMetadata.backupBasePath, unit)

if err := addFileToTar(tw, filePath, pathInArchive); err != nil {
log.Fatalln(err)
}
bar.Increment()
}
bar.Finish()
}

func writeZip(backupArchivePath string, filesToBackup []string) {
file, err := os.Create(backupArchivePath)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
zw := zip.NewWriter(file)
func writeZip(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit Unit) {
zw := zip.NewWriter(archiveFile)
defer zw.Close()

bar := pb.StartNew(len(filesToBackup))
for i := range filesToBackup {
if err := addFileToZip(zw, filesToBackup[i]); err != nil {
fileMetadata := filesToBackup[i]
filePath := fileMetadata.path

pathInArchive := getPathInArchive(filePath, fileMetadata.backupBasePath, unit)

if err := addFileToZip(zw, filePath, pathInArchive); err != nil {
log.Fatalln(err)
}
bar.Increment()
}
bar.Finish()
}

func writeBackup(filesToBackup []string, backupBasePath string, unitName string, fileExt string, addSubfolder bool) {
func writeBackup(filesToBackup []BackupFileMetadata, unit Unit) {
now := time.Now()
timeStamp := now.Format("2006-01-02_15-04")
backupBasePath := unit.destination

if addSubfolder {
newBackupBasePath := filepath.Join(backupBasePath, unitName)
if unit.addSubfolder {
newBackupBasePath := filepath.Join(unit.destination, unit.name)
pathExists := validatePath(newBackupBasePath, true)

if !pathExists {
Expand All @@ -256,19 +308,12 @@ func writeBackup(filesToBackup []string, backupBasePath string, unitName string,
backupBasePath = newBackupBasePath
}

backupArchiveName := unitName + "-" + timeStamp + "." + fileExt
backupArchiveName := unit.name + "-" + timeStamp + "." + unit.archiveType
backupArchivePath := filepath.Join(backupBasePath, backupArchiveName)

// TODO check if archive already exists. If yes, append -1 to it and try again

if fileExt == "tar.gz" {
writeTar(backupArchivePath, filesToBackup)
} else if fileExt == "zip" {
writeZip(backupArchivePath, filesToBackup)
} else {
log.Fatalf("Can't handle archive type '%s'", fileExt)
}

writeArchive(backupArchivePath, filesToBackup, unit)
log.Printf("Archive created successfully at '%s'", backupArchivePath)
}

Expand All @@ -280,7 +325,7 @@ func backupUnit(unit Unit) {
}

log.Printf("Creating backup for unit '%s'\n", unit.name)
var filesToBackup []string
var filesToBackup []BackupFileMetadata

// Check all source files from the disk in the specified source directories
for _, sourcePath := range unit.sources {
Expand All @@ -296,7 +341,7 @@ func backupUnit(unit Unit) {
log.Printf("No files found for sources in unit '%s'. Creating no backup!", unit.name)
return
}
writeBackup(filesToBackup, unit.destination, unit.name, unit.archiveType, unit.addSubfolder)
writeBackup(filesToBackup, unit)
}

func runBackup(config Config) {
Expand Down Expand Up @@ -356,7 +401,7 @@ func readConfig(configPath string) (Config, error) {
return c, validateErr
}

func addFileToTar(tw *tar.Writer, path string) error {
func addFileToTar(tw *tar.Writer, path string, pathInArchive string) error {
file, err := os.Open(path)
if err != nil {
return err
Expand All @@ -367,7 +412,7 @@ func addFileToTar(tw *tar.Writer, path string) error {
// now lets create the header as needed for this file within the tarball
header := new(tar.Header)
header.Format = tar.FormatGNU
header.Name = path
header.Name = pathInArchive
header.Size = stat.Size()
header.Mode = int64(stat.Mode())
header.ModTime = stat.ModTime()
Expand All @@ -383,7 +428,7 @@ func addFileToTar(tw *tar.Writer, path string) error {
return nil
}

func addFileToZip(zw *zip.Writer, path string) error {
func addFileToZip(zw *zip.Writer, path string, pathInArchive string) error {
file, err := os.Open(path)
if err != nil {
return err
Expand All @@ -395,7 +440,7 @@ func addFileToZip(zw *zip.Writer, path string) error {
if headerErr != nil {
return headerErr
}
header.Name = path
header.Name = pathInArchive
// write the header to the zip archive
writer, headerErr := zw.CreateHeader(header)
if headerErr != nil {
Expand Down