Fix #54 periodic backup to zip files

This commit is contained in:
Quentin McGaw
2020-05-11 23:11:48 +00:00
parent f7171e4b01
commit af68f9ba0f
6 changed files with 160 additions and 31 deletions

View File

@@ -47,6 +47,8 @@ ENV DELAY=10m \
NODE_ID=0 \
HTTP_TIMEOUT=10s \
GOTIFY_URL= \
GOTIFY_TOKEN=
GOTIFY_TOKEN= \
BACKUP_PERIOD=0 \
BACKUP_DIRECTORY=/updater/data
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
COPY --chown=1000 ui/* /updater/ui/

View File

@@ -210,6 +210,8 @@ DDNSS.de:
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`.
### Host firewall

View File

@@ -19,6 +19,7 @@ import (
libparams "github.com/qdm12/golibs/params"
"github.com/qdm12/golibs/server"
"github.com/qdm12/ddns-updater/internal/backup"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/handlers"
"github.com/qdm12/ddns-updater/internal/healthcheck"
@@ -31,12 +32,12 @@ import (
)
func main() {
os.Exit(_main(context.Background()))
os.Exit(_main(context.Background(), time.Now))
// returns 1 on error
// returns 2 on os signal
}
func _main(ctx context.Context) int {
func _main(ctx context.Context, timeNow func() time.Time) int {
if libhealthcheck.Mode(os.Args) {
// Running the program in a separate instance through the Docker
// built-in healthcheck, in an ephemeral fashion to query the
@@ -65,34 +66,7 @@ func _main(ctx context.Context) int {
return 1
}
listeningPort, warning, err := paramsReader.GetListeningPort()
if len(warning) > 0 {
logger.Warn(warning)
}
if err != nil {
logger.Error(err)
notify(4, err)
return 1
}
rootURL, err := paramsReader.GetRootURL()
if err != nil {
logger.Error(err)
notify(4, err)
return 1
}
defaultPeriod, err := paramsReader.GetDelay(libparams.Default("10m"))
if err != nil {
logger.Error(err)
notify(4, err)
return 1
}
dir, err := paramsReader.GetExeDir()
if err != nil {
logger.Error(err)
notify(4, err)
return 1
}
dataDir, err := paramsReader.GetDataDir(dir)
dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, err := getParams(paramsReader)
if err != nil {
logger.Error(err)
notify(4, err)
@@ -179,6 +153,8 @@ func _main(ctx context.Context) int {
)
}()
go backupRunLoop(ctx, backupPeriod, dir, backupDirectory, logger, timeNow)
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals,
syscall.SIGINT,
@@ -230,3 +206,69 @@ func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func
}
}, nil
}
func getParams(paramsReader params.Reader) (
dir, dataDir,
listeningPort, rootURL string,
defaultPeriod time.Duration,
backupPeriod time.Duration, backupDirectory string,
err error) {
dir, err = paramsReader.GetExeDir()
if err != nil {
return "", "", "", "", 0, 0, "", err
}
dataDir, err = paramsReader.GetDataDir(dir)
if err != nil {
return "", "", "", "", 0, 0, "", err
}
listeningPort, _, err = paramsReader.GetListeningPort()
if err != nil {
return "", "", "", "", 0, 0, "", err
}
rootURL, err = paramsReader.GetRootURL()
if err != nil {
return "", "", "", "", 0, 0, "", err
}
defaultPeriod, err = paramsReader.GetDelay(libparams.Default("10m"))
if err != nil {
return "", "", "", "", 0, 0, "", err
}
backupPeriod, err = paramsReader.GetBackupPeriod()
if err != nil {
return "", "", "", "", 0, 0, "", err
}
backupDirectory, err = paramsReader.GetBackupDirectory()
if err != nil {
return "", "", "", "", 0, 0, "", err
}
return dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, nil
}
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
logger logging.Logger, timeNow func() time.Time) {
logger = logger.WithPrefix("backup: ")
if backupPeriod == 0 {
logger.Info("disabled")
return
}
logger.Info("each %s; writing zip files to directory %s", backupPeriod, outputDir)
ziper := backup.NewZiper()
timer := time.NewTimer(backupPeriod)
for {
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
if err := ziper.ZipFiles(
filepath,
fmt.Sprintf("%s/data/updates.json", exeDir),
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
logger.Error(err)
}
select {
case <-timer.C:
timer.Reset(backupPeriod)
case <-ctx.Done():
timer.Stop()
return
}
}
}

View File

@@ -18,4 +18,6 @@ services:
- HTTP_TIMEOUT=10s
- GOTIFY_URL=
- GOTIFY_TOKEN=
- BACKUP_PERIOD=0
- BACKUP_DIRECTORY=/updater/data
restart: always

67
internal/backup/zip.go Normal file
View File

@@ -0,0 +1,67 @@
package backup
import (
"archive/zip"
"io"
"os"
)
type Ziper interface {
ZipFiles(outputFilepath string, inputFilepaths ...string) error
}
type ziper struct {
createFile func(name string) (*os.File, error)
openFile func(name string) (*os.File, error)
ioCopy func(dst io.Writer, src io.Reader) (written int64, err error)
}
func NewZiper() Ziper {
return &ziper{
createFile: os.Create,
openFile: os.Open,
ioCopy: io.Copy,
}
}
func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
f, err := z.createFile(outputFilepath)
if err != nil {
return err
}
defer f.Close()
w := zip.NewWriter(f)
defer w.Close()
for _, filepath := range inputFilepaths {
if err := z.addFile(w, filepath); err != nil {
return err
}
}
return nil
}
func (z *ziper) addFile(w *zip.Writer, filepath string) error {
f, err := z.openFile(filepath)
if err != nil {
return err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// Using FileInfoHeader() above only uses the basename of the file. If we want
// to preserve the folder structure we can overwrite this with the full path.
// header.Name = filepath
header.Method = zip.Deflate
ioWriter, err := w.CreateHeader(header)
if err != nil {
return err
}
_, err = z.ioCopy(ioWriter, f)
return err
}

View File

@@ -22,6 +22,8 @@ type Reader interface {
GetDelay(setters ...libparams.GetEnvSetter) (duration time.Duration, err error)
GetExeDir() (dir string, err error)
GetHTTPTimeout() (duration time.Duration, err error)
GetBackupPeriod() (duration time.Duration, err error)
GetBackupDirectory() (directory string, err error)
// Version getters
GetVersion() string
@@ -88,3 +90,15 @@ func (r *reader) GetExeDir() (dir string, err error) {
func (r *reader) GetHTTPTimeout() (duration time.Duration, err error) {
return r.envParams.GetHTTPTimeout(libparams.Default("10s"))
}
func (r *reader) GetBackupPeriod() (duration time.Duration, err error) {
s, err := r.envParams.GetEnv("BACKUP_PERIOD", libparams.Default("0"))
if err != nil {
return 0, err
}
return time.ParseDuration(s)
}
func (r *reader) GetBackupDirectory() (directory string, err error) {
return r.envParams.GetEnv("BACKUP_DIRECTORY", libparams.Default("./data"))
}