初始化蓝鸟文件快递柜源码与部署文档
This commit is contained in:
commit
5643d22242
70 files changed
+18669
No files matched your search
@@ -0,0 +1,211 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Backup is an offline operation: Open has already acquired the exclusive service lock.
|
||||
func (a *App) Backup(configPath, target string) error {
|
||||
abs, e := filepath.Abs(target)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
data, e := filepath.Abs(a.cfg.DataDir)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if abs == data || strings.HasPrefix(abs, data+string(os.PathSeparator)) {
|
||||
return errors.New("备份目标必须在数据目录之外")
|
||||
}
|
||||
if e = a.db.Close(); e != nil {
|
||||
return e
|
||||
}
|
||||
a.db = nil
|
||||
out, e := os.OpenFile(abs, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
success := false
|
||||
defer func() {
|
||||
out.Close()
|
||||
if !success {
|
||||
os.Remove(abs)
|
||||
}
|
||||
}()
|
||||
gz := gzip.NewWriter(out)
|
||||
tw := tar.NewWriter(gz)
|
||||
write := func(name string, f *os.File, st os.FileInfo) error {
|
||||
h := &tar.Header{Name: name, Mode: 0600, Size: st.Size(), ModTime: st.ModTime(), Typeflag: tar.TypeReg}
|
||||
if e := tw.WriteHeader(h); e != nil {
|
||||
return e
|
||||
}
|
||||
_, e := io.Copy(tw, f)
|
||||
return e
|
||||
}
|
||||
cf, e := os.Open(configPath)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
st, e := cf.Stat()
|
||||
if e == nil {
|
||||
e = write("config.toml", cf, st)
|
||||
}
|
||||
cf.Close()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
e = fs.WalkDir(a.root.FS(), ".", func(path string, d fs.DirEntry, e error) error {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if d.Type()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("拒绝备份符号链接: %s", path)
|
||||
}
|
||||
if d.IsDir() || path == "service.lock" {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(path, "tmp/") {
|
||||
return nil
|
||||
}
|
||||
st, e := d.Info()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if !st.Mode().IsRegular() {
|
||||
return errors.New("数据目录包含特殊文件")
|
||||
}
|
||||
f, e := a.root.Open(path)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer f.Close()
|
||||
return write("var/"+path, f, st)
|
||||
})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if e = tw.Close(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e = gz.Close(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e = out.Sync(); e != nil {
|
||||
return e
|
||||
}
|
||||
success = true
|
||||
return nil
|
||||
}
|
||||
func Restore(c Config, source string) error {
|
||||
if e := c.Validate(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := os.MkdirAll(c.DataDir, 0700); e != nil {
|
||||
return e
|
||||
}
|
||||
entries, e := os.ReadDir(c.DataDir)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
return errors.New("恢复数据目录必须为空")
|
||||
}
|
||||
root, e := os.OpenRoot(c.DataDir)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer root.Close()
|
||||
if e = root.WriteFile("restore.incomplete", []byte("restoring"), 0600); e != nil {
|
||||
return e
|
||||
}
|
||||
input, e := os.Open(source)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer input.Close()
|
||||
gz, e := gzip.NewReader(input)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer gz.Close()
|
||||
tr := tar.NewReader(gz)
|
||||
var total int64
|
||||
var count int
|
||||
for {
|
||||
h, e := tr.Next()
|
||||
if errors.Is(e, io.EOF) {
|
||||
break
|
||||
}
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if h.Typeflag != tar.TypeReg || !safeRelative(h.Name) {
|
||||
return errors.New("备份包含非法路径或文件类型")
|
||||
}
|
||||
count++
|
||||
if count > c.MaxRecords+32 {
|
||||
return errors.New("备份文件数量超出恢复限制")
|
||||
}
|
||||
name := ""
|
||||
if h.Name == "config.toml" {
|
||||
name = "restored-config.toml"
|
||||
} else if strings.HasPrefix(h.Name, "var/") {
|
||||
name = strings.TrimPrefix(h.Name, "var/")
|
||||
} else {
|
||||
return errors.New("未知备份路径")
|
||||
}
|
||||
if !safeRelative(name) || h.Size < 0 || h.Size > c.DataBudget || total > c.DataBudget-h.Size {
|
||||
return errors.New("备份超过恢复容量")
|
||||
}
|
||||
total += h.Size
|
||||
if e = root.MkdirAll(filepath.Dir(name), 0700); e != nil {
|
||||
return e
|
||||
}
|
||||
f, e := root.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
_, e = io.CopyN(f, tr, h.Size)
|
||||
if e == nil {
|
||||
e = f.Sync()
|
||||
}
|
||||
ce := f.Close()
|
||||
if e == nil {
|
||||
e = ce
|
||||
}
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
if _, e = io.Copy(io.Discard, gz); e != nil {
|
||||
return e
|
||||
}
|
||||
// Invalidate before Open runs lifecycle cleanup, leaving payloads retained for admin review.
|
||||
dbPath := filepath.Join(c.DataDir, "db", "bluebird.db")
|
||||
if _, e = os.Stat(dbPath); e != nil {
|
||||
return errors.New("备份缺少数据库")
|
||||
}
|
||||
a, e := OpenForRestore(c)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer a.Close()
|
||||
if e = a.Check(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e = a.InvalidateRestored(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e = root.Remove("restore.incomplete"); e != nil {
|
||||
return e
|
||||
}
|
||||
return syncDir(root, ".")
|
||||
}
|
||||
Reference in new issue
Block a user