-
-
Notifications
You must be signed in to change notification settings - Fork 18
/
serve.go
236 lines (211 loc) · 6.23 KB
/
serve.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// Copyright 2020 Changkun Ou. All rights reserved.
// Use of this source code is governed by a GPL-3.0
// license that can be found in the LICENSE file.
package rest
import (
"container/list"
"context"
"errors"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"time"
"changkun.de/x/midgard/internal/config"
"changkun.de/x/midgard/internal/utils"
)
// Midgard is the midgard server that serves all API endpoints.
type Midgard struct {
s *http.Server
mu sync.Mutex
users *list.List
}
// NewMidgard creates a new midgard server
func NewMidgard() *Midgard {
return &Midgard{users: list.New()}
}
// Serve serves Midgard RESTful APIs.
func (m *Midgard) Serve() {
ctx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
q := make(chan os.Signal, 1)
signal.Notify(q, os.Interrupt, os.Kill)
sig := <-q
log.Printf("%v", sig)
cancel()
log.Printf("shutting down api service ...")
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if err := m.s.Shutdown(ctx); err != nil && err != http.ErrServerClosed {
log.Printf("failed to shudown api service: %v", err)
}
}()
go func() {
defer wg.Done()
m.serveHTTP()
}()
go func() {
defer wg.Done()
backup(ctx)
}()
wg.Wait()
log.Printf("api server is down, good bye!")
}
func (m *Midgard) serveHTTP() {
m.s = &http.Server{Handler: m.routers(), Addr: config.S().Addr}
log.Printf("server starting at http://%s", config.S().Addr)
err := m.s.ListenAndServe()
if err != http.ErrServerClosed {
log.Printf("close with error: %v", err)
}
return
}
func init() {
if _, err := exec.LookPath("git"); err != nil {
panic("please intall git on your system: sudo apt install git")
}
}
// execute executes command inside the data folder.
func execute(dir, cmd string, args ...string) (out []byte, err error) {
c := exec.Command(cmd, args...)
c.Dir, err = filepath.Abs(dir)
if err != nil {
return nil, fmt.Errorf("cannot check your data folder: %v", err)
}
out, err = c.CombinedOutput()
return
}
const backupMsgTimeFmt = "2006-01-02 15:04"
// backup backups the data folder to a configured github repository
func backup(ctx context.Context) {
if !config.S().Store.Backup.Enable {
log.Println("backup feature is disabled.")
return
}
// initialize data as a git repo if not exists
var old = "-old"
_, err := os.Stat(config.RepoPath)
if !errors.Is(err, os.ErrNotExist) { // repo folder exists
// mkdir data/repo-old
log.Printf("mkdir %s", config.RepoPath+old)
err = os.MkdirAll(config.RepoPath+old, fs.ModeDir|fs.ModePerm)
if err != nil {
log.Fatalf("cannot rename your folder: %v", err)
}
// cp -r data/repo data/repo-old
log.Printf("cp -r %s %s", config.RepoPath, config.RepoPath+old)
err = utils.Copy(config.RepoPath, config.RepoPath+old)
if err != nil {
log.Fatalf("cannot rename your folder: %v", err)
}
// rm -rf data/repo
log.Printf("rm -rf %s", config.RepoPath)
err = os.RemoveAll(config.RepoPath)
if err != nil {
log.Fatalf("cannot remove all your old files: %v", err)
}
}
// git clone https://github.com/changkun/midgard-data repo
log.Printf("git clone %s repo", config.S().Store.Backup.Repo)
out, err := execute(config.S().Store.Path, "git", "clone",
config.S().Store.Backup.Repo, "repo")
if err != nil {
log.Println(utils.BytesToString(out))
log.Fatalf("cannot clone your data repo: %v", err)
}
// move everything to the cloned folder
// cp -r data/template data/repo
repoTmpl := config.S().Store.Path + "/template"
log.Printf("cp -r %s %s", repoTmpl, config.RepoPath)
err = utils.Copy(repoTmpl, config.RepoPath)
if err != nil {
log.Fatalf("failed to merge old data into repo folder: %v", err)
}
// cp -r data/repo-old data/repo
log.Printf("cp -r %s %s", config.RepoPath+old, config.RepoPath)
err = utils.Copy(config.RepoPath+old, config.RepoPath)
if err != nil {
log.Fatalf("failed to merge old data into repo folder: %v", err)
}
// seems ok, start commit the local changes
msg := fmt.Sprintf("midgard: backup %s", time.Now().Format(backupMsgTimeFmt))
cmds := [][]string{
{"git", "add", "."},
{"git", "commit", "-m", msg},
{"git", "push"},
}
for _, cc := range cmds {
out, err = execute(config.RepoPath, cc[0], cc[1:]...)
if err != nil {
if strings.Contains(utils.BytesToString(out), "nothing to commit") ||
strings.Contains(utils.BytesToString(out), "no changes added") {
log.Println(utils.BytesToString(out))
continue
}
log.Printf("cannot initialize your data folder: %v, details:", err)
log.Fatalf("%s: %s\n", strings.Join(cc, " "), utils.BytesToString(out))
}
}
err = os.RemoveAll(config.RepoPath + old)
if err != nil {
log.Fatalf("failed to remove your old data folder: %v", err)
}
log.Println("backup is enabled.")
t := time.NewTicker(time.Duration(config.S().Store.Backup.Interval) * time.Minute)
for {
start:
select {
case <-ctx.Done():
return
case <-t.C:
// basic conflict resolve, are there any other failures?
cmds := [][]string{
{"git", "stash"},
{"git", "fetch"},
{"git", "rebase"},
{"git", "stash", "pop"},
}
for _, cc := range cmds {
out, err = execute(config.RepoPath, cc[0], cc[1:]...)
if err != nil {
if strings.Contains(utils.BytesToString(out), "No stash entries") {
continue
}
log.Printf("failed to resolve conflict: %v, details:", err)
log.Printf("%s: %s\n", strings.Join(cc, " "), utils.BytesToString(out))
// FIXME: email notification: ask manual action (very rare?)
goto start
}
}
// add, commit, and push
msg := fmt.Sprintf("midgard: backup at %s", time.Now().Format(backupMsgTimeFmt))
cmds = [][]string{
{"git", "add", "."},
{"git", "commit", "-m", msg},
{"git", "push"},
}
for _, cc := range cmds {
out, err = execute(config.RepoPath, cc[0], cc[1:]...)
if err != nil {
if strings.Contains(utils.BytesToString(out), "nothing to commit") {
continue
}
log.Printf("cannot backup your data: %v, details:\n", err)
log.Printf("%s: %s\n", strings.Join(cc, " "), utils.BytesToString(out))
// FIXME: email notification: ask manual action (very rare?)
goto start
}
}
log.Println(msg)
}
}
}