Skip to content

Commit 39dbcdc

Browse files
committed
feat: backup option
1 parent 3284f35 commit 39dbcdc

File tree

8 files changed

+188
-82
lines changed

8 files changed

+188
-82
lines changed

config.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,43 @@
11
package main
22

3+
import (
4+
"os"
5+
"path"
6+
"time"
7+
)
8+
39
type AppConfig struct {
4-
Port int64 `yaml:"port"`
5-
ApiKey string `yaml:"api_key"`
6-
ApiSecret string `yaml:"api_secret"`
7-
Path string `yaml:"path"`
8-
Debug bool `yaml:"debug"`
9-
Compress bool `yaml:"compress"`
10-
DeleteEmptyDir bool `yaml:"delete_empty_dir"`
10+
Port int64 `yaml:"port"`
11+
ApiKey string `yaml:"api_key"`
12+
ApiSecret string `yaml:"api_secret"`
13+
Path string `yaml:"path"`
14+
Debug bool `yaml:"debug"`
15+
Compress bool `yaml:"compress"`
16+
DeleteEmptyDir bool `yaml:"delete_empty_dir"`
17+
EnableDelFileBackup bool `yaml:"enable_del_file_backup"`
18+
DelFileBackupPath string `yaml:"del_file_backup_path"`
19+
DelFileBackupDuration time.Duration `yaml:"del_file_backup_duration"`
20+
}
21+
22+
func createOrUpdateDirs() error {
23+
err := os.MkdirAll(AppCnf.Path, 0755)
24+
if err != nil {
25+
return err
26+
}
27+
28+
if AppCnf.EnableDelFileBackup {
29+
if AppCnf.DelFileBackupDuration == 0 {
30+
AppCnf.DelFileBackupDuration = time.Hour * 72
31+
}
32+
33+
if AppCnf.DelFileBackupPath == "" {
34+
AppCnf.DelFileBackupPath = path.Join(AppCnf.Path, "del_backup")
35+
}
36+
37+
err := os.MkdirAll(AppCnf.DelFileBackupPath, 0755)
38+
if err != nil {
39+
return err
40+
}
41+
}
42+
return nil
1143
}

config_sample.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,14 @@ path: ./examples/files
1010
debug: false
1111
compress: false
1212
delete_empty_dir: false
13+
# If true, then a deleted file will not be removed immediately but from DB.
14+
# Instead, it will move that one to another directory and keep for a certain period of time
15+
# This way we can recover in case it was deleted by mistake
16+
enable_del_file_backup: true
17+
# we can use a separate path for this backup
18+
# we'll use os.Rename just to change the path instated of delay on disk operation
19+
# if we're using remote disk then make sure both of the main path and del_backup are in the same disk
20+
# otherwise it will give cross-device error
21+
del_file_backup_path: "./examples/files/del_backup"
22+
# duration to keep the files as backup in hour, default: 72 hours (3 days)
23+
del_file_backup_duration: 72h

go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ go 1.23
44

55
require (
66
github.com/go-jose/go-jose/v4 v4.0.4
7-
github.com/gofiber/fiber/v2 v2.52.5
7+
github.com/gofiber/fiber/v2 v2.52.6
88
gopkg.in/yaml.v3 v3.0.1
99
)
1010

1111
require (
12-
github.com/andybalholm/brotli v1.1.0 // indirect
12+
github.com/andybalholm/brotli v1.1.1 // indirect
1313
github.com/google/uuid v1.6.0 // indirect
14-
github.com/klauspost/compress v1.17.9 // indirect
15-
github.com/mattn/go-colorable v0.1.13 // indirect
14+
github.com/klauspost/compress v1.17.11 // indirect
15+
github.com/mattn/go-colorable v0.1.14 // indirect
1616
github.com/mattn/go-isatty v0.0.20 // indirect
1717
github.com/mattn/go-runewidth v0.0.16 // indirect
1818
github.com/rivo/uniseg v0.4.7 // indirect
1919
github.com/valyala/bytebufferpool v1.0.0 // indirect
20-
github.com/valyala/fasthttp v1.55.0 // indirect
20+
github.com/valyala/fasthttp v1.58.0 // indirect
2121
github.com/valyala/tcplisten v1.0.0 // indirect
22-
golang.org/x/crypto v0.27.0 // indirect
23-
golang.org/x/sys v0.25.0 // indirect
22+
golang.org/x/crypto v0.32.0 // indirect
23+
golang.org/x/sys v0.29.0 // indirect
2424
)

go.sum

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
2-
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
1+
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
2+
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
33
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
66
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
7-
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
8-
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
7+
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
8+
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
99
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1010
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1111
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
1212
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13-
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
14-
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
15-
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
16-
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
17-
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
13+
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
14+
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
15+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
16+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
1817
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
1918
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
2019
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
@@ -28,16 +27,17 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
2827
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2928
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
3029
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
31-
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
32-
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
30+
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
31+
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
3332
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
3433
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
35-
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
36-
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
37-
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34+
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
35+
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
36+
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
37+
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
3838
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39-
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
40-
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
39+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
40+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
4141
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
4242
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4343
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

main.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ func main() {
1919

2020
err := readYaml(cnfFile)
2121
if err != nil {
22-
log.Panicln(err)
22+
log.Fatalln(err)
2323
}
2424

25-
router := Router()
25+
// create necessary dirs
26+
err = createOrUpdateDirs()
27+
if err != nil {
28+
log.Fatalln(err)
29+
}
30+
// start scheduler
31+
go startScheduler()
2632

33+
router := Router()
2734
sigChan := make(chan os.Signal, 1)
2835
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
2936

router.go

Lines changed: 42 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
"github.com/gofiber/fiber/v2"
99
"github.com/gofiber/fiber/v2/middleware/logger"
1010
"github.com/gofiber/fiber/v2/middleware/recover"
11+
"io/fs"
12+
"log"
1113
"os"
14+
"path"
1215
"strings"
1316
"time"
1417
)
@@ -34,18 +37,12 @@ func HandleDownloadFile(c *fiber.Ctx) error {
3437
token := c.Params("token")
3538

3639
if len(token) == 0 {
37-
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
38-
"status": false,
39-
"msg": "token require or invalid url",
40-
})
40+
return sendResponse(c, fiber.StatusUnauthorized, false, "token require or invalid url")
4141
}
4242

4343
file, status, err := verifyToken(token)
4444
if err != nil {
45-
return c.Status(status).JSON(fiber.Map{
46-
"status": false,
47-
"msg": err.Error(),
48-
})
45+
return sendResponse(c, status, false, err.Error())
4946
}
5047

5148
c.Attachment(file)
@@ -62,64 +59,57 @@ func HandleDeleteFile(c *fiber.Ctx) error {
6259
secret := c.Get("API-SECRET")
6360

6461
if apiKey == "" || secret == "" {
65-
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
66-
"status": false,
67-
"msg": "Auth header information are missing",
68-
})
62+
return sendResponse(c, fiber.StatusUnauthorized, false, "Auth header information are missing")
6963
}
7064

7165
if apiKey != AppCnf.ApiKey || secret != AppCnf.ApiSecret {
72-
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
73-
"status": false,
74-
"msg": "Auth header information didn't match",
75-
})
66+
return sendResponse(c, fiber.StatusUnauthorized, false, "Auth header information didn't match")
7667
}
7768

7869
req := new(DeleteFileReq)
7970
err := c.BodyParser(req)
8071
if err != nil {
81-
return c.Status(fiber.StatusNotAcceptable).JSON(fiber.Map{
82-
"status": false,
83-
"msg": err.Error(),
84-
})
72+
return sendResponse(c, fiber.StatusNotAcceptable, false, err.Error())
8573
}
8674

8775
if req.FilePath == nil {
88-
return c.Status(fiber.StatusNotAcceptable).JSON(fiber.Map{
89-
"status": false,
90-
"msg": "file_path value require",
91-
})
76+
return sendResponse(c, fiber.StatusNotAcceptable, false, "file_path value require")
9277
}
9378

9479
file := fmt.Sprintf("%s/%s", AppCnf.Path, *req.FilePath)
9580
f, err := os.Stat(file)
9681
if err != nil {
97-
if errors.Is(err, err.(*os.PathError)) {
98-
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
99-
"status": false,
100-
"msg": *req.FilePath + " does not exist",
101-
})
82+
var pathError *fs.PathError
83+
if errors.As(err, &pathError) {
84+
return sendResponse(c, fiber.StatusNotFound, false, *req.FilePath+" does not exist")
10285
} else {
103-
return c.Status(fiber.StatusNotAcceptable).JSON(fiber.Map{
104-
"status": false,
105-
"msg": err.Error(),
106-
})
86+
return sendResponse(c, fiber.StatusNotAcceptable, false, err.Error())
10787
}
10888
}
10989

11090
if f.IsDir() {
111-
return c.Status(fiber.StatusNotAcceptable).JSON(fiber.Map{
112-
"status": false,
113-
"msg": *req.FilePath + " is a directory, not allow to delete directory",
114-
})
91+
return sendResponse(c, fiber.StatusNotAcceptable, false, *req.FilePath+" is a directory, not allow to delete directory")
11592
}
11693

117-
err = os.Remove(file)
118-
if err != nil {
119-
return c.Status(fiber.StatusNotAcceptable).JSON(fiber.Map{
120-
"status": false,
121-
"msg": err.Error(),
122-
})
94+
if AppCnf.EnableDelFileBackup {
95+
// first with the video file
96+
toFile := path.Join(AppCnf.DelFileBackupPath, f.Name())
97+
err := os.Rename(file, toFile)
98+
if err != nil {
99+
log.Println(err)
100+
return sendResponse(c, fiber.StatusNotAcceptable, false, err.Error())
101+
}
102+
103+
// otherwise during cleanup will be hard to detect
104+
newTime := time.Now()
105+
if err := os.Chtimes(toFile, newTime, newTime); err != nil {
106+
log.Println("Failed to update file modification time:", err)
107+
}
108+
} else {
109+
err = os.Remove(file)
110+
if err != nil {
111+
return sendResponse(c, fiber.StatusNotAcceptable, false, err.Error())
112+
}
123113
}
124114

125115
if AppCnf.Compress {
@@ -130,17 +120,13 @@ func HandleDeleteFile(c *fiber.Ctx) error {
130120
if AppCnf.DeleteEmptyDir {
131121
dir := strings.Replace(file, "/"+f.Name(), "", 1)
132122
if dir != AppCnf.Path {
133-
empty, err := IsDirEmpty(dir)
134-
if err == nil && empty {
123+
if empty, err := IsDirEmpty(dir); err == nil && empty {
135124
_ = os.Remove(dir)
136125
}
137126
}
138127
}
139128

140-
return c.JSON(fiber.Map{
141-
"status": true,
142-
"msg": "file deleted",
143-
})
129+
return sendResponse(c, fiber.StatusOK, true, "file deleted")
144130
}
145131

146132
func verifyToken(token string) (string, int, error) {
@@ -168,3 +154,10 @@ func verifyToken(token string) (string, int, error) {
168154

169155
return file, fiber.StatusOK, nil
170156
}
157+
158+
func sendResponse(c *fiber.Ctx, statusCode int, status bool, msg string) error {
159+
return c.Status(statusCode).JSON(fiber.Map{
160+
"status": status,
161+
"msg": msg,
162+
})
163+
}

scheduler.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
"path"
7+
"time"
8+
)
9+
10+
var closeTicker chan bool
11+
12+
func startScheduler() {
13+
closeTicker = make(chan bool)
14+
hourlyChecker := time.NewTicker(1 * time.Hour)
15+
16+
defer func() {
17+
hourlyChecker.Stop()
18+
closeTicker <- true
19+
}()
20+
21+
for {
22+
select {
23+
case <-closeTicker:
24+
return
25+
case <-hourlyChecker.C:
26+
checkDelFileBackupPath()
27+
}
28+
}
29+
}
30+
31+
func checkDelFileBackupPath() {
32+
if !AppCnf.EnableDelFileBackup {
33+
// nothing to do
34+
return
35+
}
36+
37+
checkTime := time.Now().Add(-AppCnf.DelFileBackupDuration)
38+
entries, err := os.ReadDir(AppCnf.DelFileBackupPath)
39+
if err != nil {
40+
log.Println(err)
41+
return
42+
}
43+
for _, et := range entries {
44+
if et.IsDir() {
45+
continue
46+
}
47+
info, err := et.Info()
48+
if err != nil {
49+
continue
50+
}
51+
52+
if info.ModTime().Before(checkTime) {
53+
// we can remove this file
54+
fileToDelete := path.Join(AppCnf.DelFileBackupPath, et.Name())
55+
log.Println("deleting file:", fileToDelete, "because of created", checkTime, "which is older than", AppCnf.DelFileBackupDuration)
56+
57+
err = os.Remove(fileToDelete)
58+
if err != nil {
59+
log.Println(err)
60+
}
61+
}
62+
}
63+
}

version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package main
22

3-
const Version = "1.1.1"
3+
const Version = "1.2.0"

0 commit comments

Comments
 (0)