mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-12 16:38:28 -04:00
7974887295
- settings.json/session.json now store only values that differ from spec defaults - Machine-specific state moves out of settings.json into session.json - Add dms backup create/restore: tar.gz of ~/.config/DankMaterialShell fixes #3027
199 lines
4.2 KiB
Go
199 lines
4.2 KiB
Go
package backup
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const dirName = "DankMaterialShell"
|
|
|
|
func ConfigDir() (string, error) {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(configDir, dirName), nil
|
|
}
|
|
|
|
func DefaultArchiveName() string {
|
|
return fmt.Sprintf("dms-backup-%s.tar.gz", time.Now().Format("20060102-150405"))
|
|
}
|
|
|
|
func Create(outputPath string) error {
|
|
srcDir, err := ConfigDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := os.Stat(srcDir); err != nil {
|
|
return fmt.Errorf("no DMS configuration found at %s: %w", srcDir, err)
|
|
}
|
|
|
|
out, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
gz := gzip.NewWriter(out)
|
|
defer gz.Close()
|
|
tw := tar.NewWriter(gz)
|
|
defer tw.Close()
|
|
|
|
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rel, err := filepath.Rel(srcDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rel == "." {
|
|
return nil
|
|
}
|
|
|
|
var link string
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
if link, err = os.Readlink(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
header, err := tar.FileInfoHeader(info, link)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header.Name = filepath.ToSlash(filepath.Join(dirName, rel))
|
|
|
|
if err := tw.WriteHeader(header); err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return nil
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(tw, f)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func Restore(archivePath string) (string, error) {
|
|
dstDir, err := ConfigDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := validateArchive(archivePath); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
previous := ""
|
|
if _, err := os.Stat(dstDir); err == nil {
|
|
previous = dstDir + ".pre-restore-" + time.Now().Format("20060102-150405")
|
|
if err := os.Rename(dstDir, previous); err != nil {
|
|
return "", fmt.Errorf("failed to move existing configuration aside: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := extract(archivePath, filepath.Dir(dstDir)); err != nil {
|
|
if previous != "" {
|
|
os.RemoveAll(dstDir)
|
|
os.Rename(previous, dstDir)
|
|
}
|
|
return "", err
|
|
}
|
|
return previous, nil
|
|
}
|
|
|
|
func validateArchive(archivePath string) error {
|
|
hasSettings := false
|
|
err := walkArchive(archivePath, func(header *tar.Header, _ *tar.Reader) error {
|
|
name := filepath.ToSlash(filepath.Clean(header.Name))
|
|
if strings.HasPrefix(name, "..") || filepath.IsAbs(header.Name) {
|
|
return fmt.Errorf("unsafe path in archive: %s", header.Name)
|
|
}
|
|
if !strings.HasPrefix(name, dirName+"/") && name != dirName {
|
|
return fmt.Errorf("not a DMS backup: unexpected entry %s", header.Name)
|
|
}
|
|
if name == dirName+"/settings.json" {
|
|
hasSettings = true
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !hasSettings {
|
|
return fmt.Errorf("not a DMS backup: settings.json missing from archive")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func extract(archivePath, destParent string) error {
|
|
return walkArchive(archivePath, func(header *tar.Header, tr *tar.Reader) error {
|
|
target := filepath.Join(destParent, filepath.Clean(header.Name))
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
return os.MkdirAll(target, os.FileMode(header.Mode))
|
|
case tar.TypeSymlink:
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return os.Symlink(header.Linkname, target)
|
|
case tar.TypeReg:
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
return err
|
|
}
|
|
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(f, tr)
|
|
return err
|
|
default:
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
func walkArchive(archivePath string, visit func(*tar.Header, *tar.Reader) error) error {
|
|
f, err := os.Open(archivePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
gz, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return fmt.Errorf("not a gzip archive: %w", err)
|
|
}
|
|
defer gz.Close()
|
|
|
|
tr := tar.NewReader(gz)
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := visit(header, tr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|