Skip to content

Commit 4a6d279

Browse files
authored
feat: modularize backmeup into smaller packages (#17)
Merge pull request #17 from d-Rickyy-b/modularize
2 parents 5d75752 + 83b3a0f commit 4a6d279

File tree

5 files changed

+420
-337
lines changed

5 files changed

+420
-337
lines changed

CHANGELOG.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Fixed
1212
### Docs
1313

14+
## [0.1.4]
15+
### Added
16+
- Symlink support for tar files ([5d75752](https://github.com/d-Rickyy-b/backmeup/commit/5d757525bbde26429e90a30ea5fba8d721db6f72))
17+
### Changed
18+
- Move archive code to archiver package ([d8666cb](https://github.com/d-Rickyy-b/backmeup/commit/d8666cb5d3acc25a77f3d84f92c52301687dd6ae))
19+
- Move config code to config package ([0a03807](https://github.com/d-Rickyy-b/backmeup/commit/0a038077a21c88781abf77b85a6a9da7b60df9f6))
20+
### Fixed
21+
- Add compression for zip files ([52733bc](https://github.com/d-Rickyy-b/backmeup/commit/52733bc0dc4e1378e02467c3712ffe05b6cb3fd2))
22+
- Replace Fatalln with Println ([721f6b2](https://github.com/d-Rickyy-b/backmeup/commit/721f6b27d1501b403d94f1273639a7a1a92b8b76))
23+
- Correctly assign 'verbose' and 'debug' variables ([cd11006](https://github.com/d-Rickyy-b/backmeup/commit/cd110062d8f619ead0f63b4a663c3a46aedbd228))
24+
- Only store regular files in tar archives ([d9b26fc](https://github.com/d-Rickyy-b/backmeup/commit/d9b26fc5d0b465bebec05454fecbe4b5b14538b9))
25+
1426
## [0.1.3] - 2020-12-22
1527
### Added
1628
- Ability to generate an archive with relative paths ([#13](https://github.com/d-Rickyy-b/backmeup/pull/13))
@@ -35,8 +47,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3547
## [0.1.0] - 2020-11-17
3648
Initial release! First usable version of backmeup is published as v0.1.0
3749

38-
[unreleased]: https://github.com/d-Rickyy-b/backmeup/compare/v0.1.3...HEAD
39-
[0.1.3]: https://github.com/d-Rickyy-b/backmeup/tree/v0.1.3
40-
[0.1.2]: https://github.com/d-Rickyy-b/backmeup/tree/v0.1.2
41-
[0.1.1]: https://github.com/d-Rickyy-b/backmeup/tree/v0.1.1
50+
[unreleased]: https://github.com/d-Rickyy-b/backmeup/compare/v0.1.4...HEAD
51+
[0.1.4]: https://github.com/d-Rickyy-b/backmeup/compare/v0.1.3...v0.1.4
52+
[0.1.3]: https://github.com/d-Rickyy-b/backmeup/compare/v0.1.2...v0.1.3
53+
[0.1.2]: https://github.com/d-Rickyy-b/backmeup/compare/v0.1.1...v0.1.2
54+
[0.1.1]: https://github.com/d-Rickyy-b/backmeup/compare/v0.1.0...v0.1.1
4255
[0.1.0]: https://github.com/d-Rickyy-b/backmeup/tree/v0.1.0

archiver/archive.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package archiver
2+
3+
import (
4+
"archive/tar"
5+
"backmeup/config"
6+
"fmt"
7+
"github.com/cheggaaa/pb/v3"
8+
"github.com/klauspost/compress/gzip"
9+
"github.com/klauspost/compress/zip"
10+
"io"
11+
"log"
12+
"os"
13+
"path/filepath"
14+
"strings"
15+
)
16+
17+
type BackupFileMetadata struct {
18+
Path string
19+
BackupBasePath string
20+
}
21+
22+
func getPathInArchive(filePath string, backupBasePath string, unit config.Unit) string {
23+
// Remove the base Path from the file Path within the archiver, if option is set
24+
pathInArchive := filePath
25+
26+
if !unit.UseAbsolutePaths {
27+
parentBasePath := filepath.Dir(backupBasePath)
28+
pathInArchive = strings.ReplaceAll(filePath, parentBasePath, "")
29+
30+
pathInArchive = strings.TrimPrefix(pathInArchive, "\\")
31+
}
32+
33+
return pathInArchive
34+
}
35+
36+
func WriteArchive(backupArchivePath string, filesToBackup []BackupFileMetadata, unit config.Unit) {
37+
archiveFile, err := os.Create(backupArchivePath)
38+
if err != nil {
39+
log.Fatalln(err)
40+
}
41+
defer archiveFile.Close()
42+
43+
switch unit.ArchiveType {
44+
case "tar.gz":
45+
writeTar(archiveFile, filesToBackup, unit)
46+
case "zip":
47+
writeZip(archiveFile, filesToBackup, unit)
48+
default:
49+
log.Fatalf("Can't handle archiver type '%s'", unit.ArchiveType)
50+
}
51+
}
52+
53+
func writeTar(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit config.Unit) {
54+
// set up the gzip and tar writer
55+
gw := gzip.NewWriter(archiveFile)
56+
defer gw.Close()
57+
58+
tw := tar.NewWriter(gw)
59+
defer tw.Close()
60+
61+
// Init progress bar
62+
bar := pb.StartNew(len(filesToBackup))
63+
64+
for i := range filesToBackup {
65+
fileMetadata := filesToBackup[i]
66+
filePath := fileMetadata.Path
67+
68+
pathInArchive := getPathInArchive(filePath, fileMetadata.BackupBasePath, unit)
69+
70+
if err := addFileToTar(tw, filePath, pathInArchive); err != nil {
71+
log.Println(err)
72+
}
73+
74+
bar.Increment()
75+
}
76+
77+
bar.Finish()
78+
}
79+
80+
func writeZip(archiveFile *os.File, filesToBackup []BackupFileMetadata, unit config.Unit) {
81+
zw := zip.NewWriter(archiveFile)
82+
defer zw.Close()
83+
84+
bar := pb.StartNew(len(filesToBackup))
85+
86+
for i := range filesToBackup {
87+
fileMetadata := filesToBackup[i]
88+
filePath := fileMetadata.Path
89+
90+
pathInArchive := getPathInArchive(filePath, fileMetadata.BackupBasePath, unit)
91+
92+
if err := addFileToZip(zw, filePath, pathInArchive); err != nil {
93+
log.Println(err)
94+
}
95+
96+
bar.Increment()
97+
}
98+
99+
bar.Finish()
100+
}
101+
102+
func addFileToTar(tw *tar.Writer, path string, pathInArchive string) error {
103+
file, err := os.Open(path)
104+
if err != nil {
105+
return err
106+
}
107+
defer file.Close()
108+
109+
if stat, err := os.Lstat(path); err == nil {
110+
var linkTarget string
111+
// Check if file is symlink
112+
if stat.Mode()&os.ModeSymlink != 0 {
113+
log.Printf("Found link: %s", path)
114+
var err error
115+
linkTarget, err = os.Readlink(path)
116+
if err != nil {
117+
return fmt.Errorf("%s: readlink: %v", stat.Name(), err)
118+
}
119+
}
120+
121+
// now lets create the header as needed for this file within the tarball
122+
header, err := tar.FileInfoHeader(stat, filepath.ToSlash(linkTarget))
123+
if err != nil {
124+
return nil
125+
}
126+
header.Name = pathInArchive
127+
128+
// write the header to the tarball archiver
129+
if err := tw.WriteHeader(header); err != nil {
130+
return err
131+
}
132+
133+
// Check for regular files
134+
if header.Typeflag == tar.TypeReg {
135+
// copy the file data to the tarball
136+
_, err := io.Copy(tw, file)
137+
if err != nil {
138+
return fmt.Errorf("%s: copying contents: %w", file.Name(), err)
139+
}
140+
}
141+
}
142+
143+
return nil
144+
}
145+
146+
func addFileToZip(zw *zip.Writer, path string, pathInArchive string) error {
147+
file, err := os.Open(path)
148+
if err != nil {
149+
return err
150+
}
151+
defer file.Close()
152+
153+
if stat, err := file.Stat(); err == nil {
154+
header, headerErr := zip.FileInfoHeader(stat)
155+
if headerErr != nil {
156+
return headerErr
157+
}
158+
159+
header.Method = zip.Deflate
160+
header.Name = pathInArchive
161+
// write the header to the zip archiver
162+
writer, headerErr := zw.CreateHeader(header)
163+
if headerErr != nil {
164+
return err
165+
}
166+
// copy the file data to the zip
167+
if _, err := io.Copy(writer, file); err != nil {
168+
return err
169+
}
170+
} else {
171+
return err
172+
}
173+
174+
return nil
175+
}

bkperrors/errors.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package bkperrors
2+
3+
import "errors"
4+
5+
var (
6+
ErrCannotAccessSrcDir = errors.New("can't access source directory")
7+
ErrCannotAccessDstDir = errors.New("can't access source directory")
8+
)

config/config.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package config
2+
3+
import (
4+
"backmeup/bkperrors"
5+
"gopkg.in/yaml.v2"
6+
"io/ioutil"
7+
"log"
8+
"os"
9+
)
10+
11+
type Unit struct {
12+
Name string
13+
Sources []string
14+
Destination string
15+
Excludes []string
16+
ArchiveType string
17+
AddSubfolder bool
18+
Enabled bool
19+
UseAbsolutePaths bool
20+
}
21+
22+
type Config struct {
23+
Units []Unit
24+
}
25+
26+
// Helper struct for parsing the yaml
27+
type yamlUnit struct {
28+
Sources *[]string `yaml:"sources"`
29+
Destination *string `yaml:"destination"`
30+
Excludes *[]string `yaml:"excludes"`
31+
ArchiveType *string `yaml:"archive_type"`
32+
AddSubfolder *bool `yaml:"add_subfolder"`
33+
Enabled *bool `yaml:"enabled"`
34+
UseAbsolutePaths *bool `yaml:"use_absolute_paths"`
35+
}
36+
37+
func (config Config) FromYaml(yamlData []byte) (Config, error) {
38+
// Create a config object from yaml byte array
39+
unitMap := make(map[string]yamlUnit)
40+
41+
log.Println("Parsing config yaml")
42+
43+
unmarshalErr := yaml.Unmarshal(yamlData, &unitMap)
44+
if unmarshalErr != nil {
45+
log.Fatalf("Unmarshal error: %v", unmarshalErr)
46+
}
47+
48+
// After parsing the yaml into unitMap, we iterate over all available units
49+
for unitName, yamlUnit := range unitMap {
50+
unit := Unit{}
51+
52+
// Set defaults
53+
unit.Enabled = true
54+
if yamlUnit.Enabled != nil {
55+
unit.Enabled = *yamlUnit.Enabled
56+
}
57+
58+
unit.AddSubfolder = false
59+
if yamlUnit.AddSubfolder != nil {
60+
unit.AddSubfolder = *yamlUnit.AddSubfolder
61+
}
62+
63+
unit.ArchiveType = "tar.gz"
64+
if yamlUnit.ArchiveType != nil {
65+
unit.ArchiveType = *yamlUnit.ArchiveType
66+
}
67+
68+
unit.Excludes = []string{}
69+
if yamlUnit.Excludes != nil {
70+
unit.Excludes = *yamlUnit.Excludes
71+
}
72+
73+
unit.UseAbsolutePaths = true
74+
if yamlUnit.UseAbsolutePaths != nil {
75+
unit.UseAbsolutePaths = *yamlUnit.UseAbsolutePaths
76+
}
77+
78+
if yamlUnit.Sources == nil || yamlUnit.Destination == nil {
79+
log.Fatalf("Sources or destination can't be parsed for unit '%s'", unitName)
80+
} else {
81+
unit.Sources = *yamlUnit.Sources
82+
unit.Destination = *yamlUnit.Destination
83+
}
84+
85+
unit.Name = unitName
86+
87+
config.Units = append(config.Units, unit)
88+
}
89+
90+
return config, nil
91+
}
92+
93+
func validatePath(path string, mustBeDir bool) bool {
94+
// Checks if a file/directory exists
95+
file, err := os.Stat(path)
96+
97+
if err != nil {
98+
if os.IsNotExist(err) {
99+
log.Printf("File '%s' does not exist.", path)
100+
}
101+
102+
return false
103+
}
104+
105+
if mustBeDir {
106+
return file.IsDir()
107+
}
108+
109+
return true
110+
}
111+
112+
func (config *Config) validate() error {
113+
// Check if the config is valid and can be used for backups
114+
// TODO maybe skip missing sources via param
115+
log.Println("Validating config!")
116+
117+
for _, unit := range config.Units {
118+
if !unit.Enabled {
119+
log.Printf("Unit '%s' is disabled. Skip validation for this unit!", unit.Name)
120+
121+
continue
122+
}
123+
124+
for _, sourcePath := range unit.Sources {
125+
// Each source path must be an existing directory!
126+
if !validatePath(sourcePath, true) {
127+
log.Printf("The given source path ('%s') does not exist or is no directory!", sourcePath)
128+
129+
return bkperrors.ErrCannotAccessSrcDir
130+
}
131+
}
132+
// Also the destination path must exist!
133+
if !validatePath(unit.Destination, true) {
134+
log.Printf("The given destination path ('%s') does not exist or is no directory!", unit.Destination)
135+
136+
return bkperrors.ErrCannotAccessDstDir
137+
}
138+
139+
log.Printf("Unit '%s' is valid!", unit.Name)
140+
}
141+
142+
return nil
143+
}
144+
145+
func ReadConfig(configPath string) (Config, error) {
146+
// Read config file at configPath
147+
log.Printf("Trying to read config file '%s'!", configPath)
148+
data, err := ioutil.ReadFile(configPath)
149+
150+
if err != nil {
151+
log.Println("Can't read config file! Exiting!")
152+
os.Exit(1)
153+
}
154+
155+
// Read config file to Config struct
156+
c := Config{}
157+
c, err = c.FromYaml(data)
158+
159+
if err != nil {
160+
return c, err
161+
}
162+
163+
validateErr := c.validate()
164+
165+
return c, validateErr
166+
}

0 commit comments

Comments
 (0)