Skip to content

Commit af68f9b

Browse files
committed
Fix #54 periodic backup to zip files
1 parent f7171e4 commit af68f9b

File tree

6 files changed

+160
-31
lines changed

6 files changed

+160
-31
lines changed

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ ENV DELAY=10m \
4747
NODE_ID=0 \
4848
HTTP_TIMEOUT=10s \
4949
GOTIFY_URL= \
50-
GOTIFY_TOKEN=
50+
GOTIFY_TOKEN= \
51+
BACKUP_PERIOD=0 \
52+
BACKUP_DIRECTORY=/updater/data
5153
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
5254
COPY --chown=1000 ui/* /updater/ui/

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ DDNSS.de:
210210
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
211211
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
212212
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
213+
| `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 |
214+
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`.
213215
214216
### Host firewall
215217

cmd/updater/main.go

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
libparams "github.com/qdm12/golibs/params"
2020
"github.com/qdm12/golibs/server"
2121

22+
"github.com/qdm12/ddns-updater/internal/backup"
2223
"github.com/qdm12/ddns-updater/internal/data"
2324
"github.com/qdm12/ddns-updater/internal/handlers"
2425
"github.com/qdm12/ddns-updater/internal/healthcheck"
@@ -31,12 +32,12 @@ import (
3132
)
3233

3334
func main() {
34-
os.Exit(_main(context.Background()))
35+
os.Exit(_main(context.Background(), time.Now))
3536
// returns 1 on error
3637
// returns 2 on os signal
3738
}
3839

39-
func _main(ctx context.Context) int {
40+
func _main(ctx context.Context, timeNow func() time.Time) int {
4041
if libhealthcheck.Mode(os.Args) {
4142
// Running the program in a separate instance through the Docker
4243
// built-in healthcheck, in an ephemeral fashion to query the
@@ -65,34 +66,7 @@ func _main(ctx context.Context) int {
6566
return 1
6667
}
6768

68-
listeningPort, warning, err := paramsReader.GetListeningPort()
69-
if len(warning) > 0 {
70-
logger.Warn(warning)
71-
}
72-
if err != nil {
73-
logger.Error(err)
74-
notify(4, err)
75-
return 1
76-
}
77-
rootURL, err := paramsReader.GetRootURL()
78-
if err != nil {
79-
logger.Error(err)
80-
notify(4, err)
81-
return 1
82-
}
83-
defaultPeriod, err := paramsReader.GetDelay(libparams.Default("10m"))
84-
if err != nil {
85-
logger.Error(err)
86-
notify(4, err)
87-
return 1
88-
}
89-
dir, err := paramsReader.GetExeDir()
90-
if err != nil {
91-
logger.Error(err)
92-
notify(4, err)
93-
return 1
94-
}
95-
dataDir, err := paramsReader.GetDataDir(dir)
69+
dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, err := getParams(paramsReader)
9670
if err != nil {
9771
logger.Error(err)
9872
notify(4, err)
@@ -179,6 +153,8 @@ func _main(ctx context.Context) int {
179153
)
180154
}()
181155

156+
go backupRunLoop(ctx, backupPeriod, dir, backupDirectory, logger, timeNow)
157+
182158
osSignals := make(chan os.Signal, 1)
183159
signal.Notify(osSignals,
184160
syscall.SIGINT,
@@ -230,3 +206,69 @@ func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func
230206
}
231207
}, nil
232208
}
209+
210+
func getParams(paramsReader params.Reader) (
211+
dir, dataDir,
212+
listeningPort, rootURL string,
213+
defaultPeriod time.Duration,
214+
backupPeriod time.Duration, backupDirectory string,
215+
err error) {
216+
dir, err = paramsReader.GetExeDir()
217+
if err != nil {
218+
return "", "", "", "", 0, 0, "", err
219+
}
220+
dataDir, err = paramsReader.GetDataDir(dir)
221+
if err != nil {
222+
return "", "", "", "", 0, 0, "", err
223+
}
224+
listeningPort, _, err = paramsReader.GetListeningPort()
225+
if err != nil {
226+
return "", "", "", "", 0, 0, "", err
227+
}
228+
rootURL, err = paramsReader.GetRootURL()
229+
if err != nil {
230+
return "", "", "", "", 0, 0, "", err
231+
}
232+
defaultPeriod, err = paramsReader.GetDelay(libparams.Default("10m"))
233+
if err != nil {
234+
return "", "", "", "", 0, 0, "", err
235+
}
236+
237+
backupPeriod, err = paramsReader.GetBackupPeriod()
238+
if err != nil {
239+
return "", "", "", "", 0, 0, "", err
240+
}
241+
backupDirectory, err = paramsReader.GetBackupDirectory()
242+
if err != nil {
243+
return "", "", "", "", 0, 0, "", err
244+
}
245+
return dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, nil
246+
}
247+
248+
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
249+
logger logging.Logger, timeNow func() time.Time) {
250+
logger = logger.WithPrefix("backup: ")
251+
if backupPeriod == 0 {
252+
logger.Info("disabled")
253+
return
254+
}
255+
logger.Info("each %s; writing zip files to directory %s", backupPeriod, outputDir)
256+
ziper := backup.NewZiper()
257+
timer := time.NewTimer(backupPeriod)
258+
for {
259+
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
260+
if err := ziper.ZipFiles(
261+
filepath,
262+
fmt.Sprintf("%s/data/updates.json", exeDir),
263+
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
264+
logger.Error(err)
265+
}
266+
select {
267+
case <-timer.C:
268+
timer.Reset(backupPeriod)
269+
case <-ctx.Done():
270+
timer.Stop()
271+
return
272+
}
273+
}
274+
}

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ services:
1818
- HTTP_TIMEOUT=10s
1919
- GOTIFY_URL=
2020
- GOTIFY_TOKEN=
21+
- BACKUP_PERIOD=0
22+
- BACKUP_DIRECTORY=/updater/data
2123
restart: always

internal/backup/zip.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package backup
2+
3+
import (
4+
"archive/zip"
5+
"io"
6+
"os"
7+
)
8+
9+
type Ziper interface {
10+
ZipFiles(outputFilepath string, inputFilepaths ...string) error
11+
}
12+
13+
type ziper struct {
14+
createFile func(name string) (*os.File, error)
15+
openFile func(name string) (*os.File, error)
16+
ioCopy func(dst io.Writer, src io.Reader) (written int64, err error)
17+
}
18+
19+
func NewZiper() Ziper {
20+
return &ziper{
21+
createFile: os.Create,
22+
openFile: os.Open,
23+
ioCopy: io.Copy,
24+
}
25+
}
26+
27+
func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
28+
f, err := z.createFile(outputFilepath)
29+
if err != nil {
30+
return err
31+
}
32+
defer f.Close()
33+
w := zip.NewWriter(f)
34+
defer w.Close()
35+
for _, filepath := range inputFilepaths {
36+
if err := z.addFile(w, filepath); err != nil {
37+
return err
38+
}
39+
}
40+
return nil
41+
}
42+
43+
func (z *ziper) addFile(w *zip.Writer, filepath string) error {
44+
f, err := z.openFile(filepath)
45+
if err != nil {
46+
return err
47+
}
48+
defer f.Close()
49+
info, err := f.Stat()
50+
if err != nil {
51+
return err
52+
}
53+
header, err := zip.FileInfoHeader(info)
54+
if err != nil {
55+
return err
56+
}
57+
// Using FileInfoHeader() above only uses the basename of the file. If we want
58+
// to preserve the folder structure we can overwrite this with the full path.
59+
// header.Name = filepath
60+
header.Method = zip.Deflate
61+
ioWriter, err := w.CreateHeader(header)
62+
if err != nil {
63+
return err
64+
}
65+
_, err = z.ioCopy(ioWriter, f)
66+
return err
67+
}

internal/params/params.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type Reader interface {
2222
GetDelay(setters ...libparams.GetEnvSetter) (duration time.Duration, err error)
2323
GetExeDir() (dir string, err error)
2424
GetHTTPTimeout() (duration time.Duration, err error)
25+
GetBackupPeriod() (duration time.Duration, err error)
26+
GetBackupDirectory() (directory string, err error)
2527

2628
// Version getters
2729
GetVersion() string
@@ -88,3 +90,15 @@ func (r *reader) GetExeDir() (dir string, err error) {
8890
func (r *reader) GetHTTPTimeout() (duration time.Duration, err error) {
8991
return r.envParams.GetHTTPTimeout(libparams.Default("10s"))
9092
}
93+
94+
func (r *reader) GetBackupPeriod() (duration time.Duration, err error) {
95+
s, err := r.envParams.GetEnv("BACKUP_PERIOD", libparams.Default("0"))
96+
if err != nil {
97+
return 0, err
98+
}
99+
return time.ParseDuration(s)
100+
}
101+
102+
func (r *reader) GetBackupDirectory() (directory string, err error) {
103+
return r.envParams.GetEnv("BACKUP_DIRECTORY", libparams.Default("./data"))
104+
}

0 commit comments

Comments
 (0)