Skip to content

Commit

Permalink
feat: various CLI and feature improvements (#23)
Browse files Browse the repository at this point in the history
Merge pull request #23 from d-Rickyy-b/cli-improvements
  • Loading branch information
d-Rickyy-b authored Feb 11, 2021
2 parents 4a6d279 + 8f1a23c commit b3c99ef
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 36 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@
config.yml

# Exclude compiled binaries
backmeup_*-*
backmeup_*-*

# Exclude archive files
*.zip
*.tar.gz
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
### Docs

## [0.1.4]
## [0.1.4] - 2021-02-11
### Added
- Symlink support for tar files ([5d75752](https://github.com/d-Rickyy-b/backmeup/commit/5d757525bbde26429e90a30ea5fba8d721db6f72))
- Following symlinks (aka replacing a symlink to a file with the actual file) ([a98fe65](https://github.com/d-Rickyy-b/backmeup/commit/a98fe65d8188cd8f5abac2d766cffa594c032757))
- Ability to only run backups of certain units via `-u`/`--unit` CLI parameter ([92db794](https://github.com/d-Rickyy-b/backmeup/commit/92db794365448c67379f20ff3e2d6bfb998f1f57))
- Check if archive already exists ([fd88626](https://github.com/d-Rickyy-b/backmeup/commit/fd886263038d6c97cb0f481e9ff0140187d5283e))
- Add `-t`/`--test-path` CLI parameter for checking exclusion for given paths ([1b13e44](https://github.com/d-Rickyy-b/backmeup/commit/1b13e44a38faa0e472ecaea4b8864cfffc2ab147))
- Add `-v`/`--version` CLI parameter to just print the tool's version ([2c51b05](https://github.com/d-Rickyy-b/backmeup/commit/2c51b058723e1eb3e46ba2e8ee0b2260ad39b362))
### Changed
- Move archive code to archiver package ([d8666cb](https://github.com/d-Rickyy-b/backmeup/commit/d8666cb5d3acc25a77f3d84f92c52301687dd6ae))
- Move config code to config package ([0a03807](https://github.com/d-Rickyy-b/backmeup/commit/0a038077a21c88781abf77b85a6a9da7b60df9f6))
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
![backmeup logo](https://raw.githubusercontent.com/d-Rickyy-b/backmeup/master/docs/backmeup_logo_transparent.png)
# backmeup - a lightweight backup utility for the CLI
[![build](https://github.com/d-Rickyy-b/backmeup/workflows/build/badge.svg)](https://github.com/d-Rickyy-b/backmeup/actions?query=workflow%3Abuild)

When managing several servers, you often find yourself in a need of making backups. I searched for tools online that could make it into a painless experience but never found an easy-to-use, lightweight, portable, CLI tool which is easy to configure and does not need a remote server for backups.
When managing several servers, you often find yourself in a need of making backups.
I searched for tools online that could make it into a painless experience but never found an easy-to-use, lightweight, portable, CLI tool which is easy to configure and does not need a remote server for backups.
That's why I created **backmeup**.

### Key features
- Easy to use
- Define multiple backups in a single config file
- **Portable** - you can copy the **single executable** with a configuration file on all your machines
- **Lightweight** - the executables are ~1 mb
- **Lightweight** - the executables are < 10 mb
- Exclude files and paths with .gitignore-like syntax
- Group together multiple source paths into one backup
- Config files written in yaml
Expand Down Expand Up @@ -115,6 +117,7 @@ The name of your backup is the key at root level. Starting from there you can co
| 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)) |
| follow_symlinks | boolean | No | `false` | If set to `true`, the targets of symlinks (regular files only) will be included in the archive. Only works for tar archives! |

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.
57 changes: 39 additions & 18 deletions archiver/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ type BackupFileMetadata struct {
BackupBasePath string
}

func getPathInArchive(filePath string, backupBasePath string, unit config.Unit) string {
var currentUnitConfig config.Unit

func getPathInArchive(filePath string, backupBasePath string) string {
// Remove the base Path from the file Path within the archiver, if option is set
pathInArchive := filePath

if !unit.UseAbsolutePaths {
if !currentUnitConfig.UseAbsolutePaths {
parentBasePath := filepath.Dir(backupBasePath)
pathInArchive = strings.ReplaceAll(filePath, parentBasePath, "")

Expand All @@ -34,6 +36,8 @@ func getPathInArchive(filePath string, backupBasePath string, unit config.Unit)
}

func WriteArchive(backupArchivePath string, filesToBackup []BackupFileMetadata, unit config.Unit) {
// Store the current config for other methods to access config parameters
currentUnitConfig = unit
archiveFile, err := os.Create(backupArchivePath)
if err != nil {
log.Fatalln(err)
Expand All @@ -42,15 +46,15 @@ func WriteArchive(backupArchivePath string, filesToBackup []BackupFileMetadata,

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

func writeTar(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit config.Unit) {
func writeTar(archiveFile *os.File, filesToBackup []BackupFileMetadata) {
// set up the gzip and tar writer
gw := gzip.NewWriter(archiveFile)
defer gw.Close()
Expand All @@ -59,13 +63,15 @@ func writeTar(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit con
defer tw.Close()

// Init progress bar
bar := pb.StartNew(len(filesToBackup))
bar := pb.New(len(filesToBackup))
bar.SetMaxWidth(100)
bar.Start()

for i := range filesToBackup {
fileMetadata := filesToBackup[i]
filePath := fileMetadata.Path

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

if err := addFileToTar(tw, filePath, pathInArchive); err != nil {
log.Println(err)
Expand All @@ -77,17 +83,19 @@ func writeTar(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit con
bar.Finish()
}

func writeZip(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit config.Unit) {
func writeZip(archiveFile *os.File, filesToBackup []BackupFileMetadata) {
zw := zip.NewWriter(archiveFile)
defer zw.Close()

bar := pb.StartNew(len(filesToBackup))
bar := pb.New(len(filesToBackup))
bar.SetMaxWidth(100)
bar.Start()

for i := range filesToBackup {
fileMetadata := filesToBackup[i]
filePath := fileMetadata.Path

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

if err := addFileToZip(zw, filePath, pathInArchive); err != nil {
log.Println(err)
Expand All @@ -100,23 +108,36 @@ func writeZip(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit con
}

func addFileToTar(tw *tar.Writer, path string, pathInArchive string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

if stat, err := os.Lstat(path); err == nil {
var linkTarget string
// Check if file is symlink
if stat.Mode()&os.ModeSymlink != 0 {
log.Printf("Found link: %s", path)
var err error
linkTarget, err = os.Readlink(path)
if err != nil {
return fmt.Errorf("%s: readlink: %v", stat.Name(), err)
}

// In case the user wants to follow symlinks we eval the symlink target
if currentUnitConfig.FollowSymlinks {
if linkTargetPath, err := filepath.EvalSymlinks(path); err == nil {
if linkTargetInfo, statErr := os.Stat(linkTargetPath); statErr == nil {
if linkTargetInfo.Mode().IsRegular() {
// If file is regular, we can simply replace the symlink with the actual file
path = linkTargetPath
linkTarget = ""
stat = linkTargetInfo
}
}
}
}
}

file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

// now lets create the header as needed for this file within the tarball
header, err := tar.FileInfoHeader(stat, filepath.ToSlash(linkTarget))
Expand All @@ -131,7 +152,7 @@ func addFileToTar(tw *tar.Writer, path string, pathInArchive string) error {
}

// Check for regular files
if header.Typeflag == tar.TypeReg {
if stat.Mode().IsRegular() {
// copy the file data to the tarball
_, err := io.Copy(tw, file)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions config.sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ backup_unit_name:
- "*.rar"
archive_type: "tar.gz"
add_subfolder: false
follow_symlinks: false
enabled: true

other_unit:
Expand Down
14 changes: 11 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Unit struct {
AddSubfolder bool
Enabled bool
UseAbsolutePaths bool
FollowSymlinks bool
}

type Config struct {
Expand All @@ -32,10 +33,11 @@ type yamlUnit struct {
AddSubfolder *bool `yaml:"add_subfolder"`
Enabled *bool `yaml:"enabled"`
UseAbsolutePaths *bool `yaml:"use_absolute_paths"`
FollowSymlinks *bool `yaml:"follow_symlinks"`
}

// FromYaml creates a config struct from a given yaml file as bytes
func (config Config) FromYaml(yamlData []byte) (Config, error) {
// Create a config object from yaml byte array
unitMap := make(map[string]yamlUnit)

log.Println("Parsing config yaml")
Expand Down Expand Up @@ -75,6 +77,11 @@ func (config Config) FromYaml(yamlData []byte) (Config, error) {
unit.UseAbsolutePaths = *yamlUnit.UseAbsolutePaths
}

unit.FollowSymlinks = false
if yamlUnit.FollowSymlinks != nil {
unit.FollowSymlinks = *yamlUnit.FollowSymlinks
}

if yamlUnit.Sources == nil || yamlUnit.Destination == nil {
log.Fatalf("Sources or destination can't be parsed for unit '%s'", unitName)
} else {
Expand All @@ -90,8 +97,9 @@ func (config Config) FromYaml(yamlData []byte) (Config, error) {
return config, nil
}

// validatePath checks if a given file/directory exists
// It returns true if it exists, otherwise false
func validatePath(path string, mustBeDir bool) bool {
// Checks if a file/directory exists
file, err := os.Stat(path)

if err != nil {
Expand Down Expand Up @@ -142,8 +150,8 @@ func (config *Config) validate() error {
return nil
}

// ReadConfig reads a config file from a given path
func ReadConfig(configPath string) (Config, error) {
// Read config file at configPath
log.Printf("Trying to read config file '%s'!", configPath)
data, err := ioutil.ReadFile(configPath)

Expand Down
Binary file added docs/backmeup_logo_transparent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b3c99ef

Please sign in to comment.