aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.go5
-rw-r--r--gpg/gpg.go35
-rw-r--r--main.go22
-rw-r--r--objects/id.go10
-rw-r--r--objects/object_snapshot.go17
-rw-r--r--restore_dir.go12
-rw-r--r--snapshot.go275
-rw-r--r--storage/storage.go37
-rw-r--r--write_dir.go9
9 files changed, 400 insertions, 22 deletions
diff --git a/config/config.go b/config/config.go
index e925be3..7454d7a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,6 +1,7 @@
package config
import (
+ "code.laria.me/petrific/gpg"
"fmt"
"github.com/BurntSushi/toml"
"github.com/adrg/xdg"
@@ -32,6 +33,10 @@ func LoadConfig(path string) (config Config, err error) {
return
}
+func (c Config) GPGSigner() gpg.Signer {
+ return gpg.Signer{c.Signing.Key}
+}
+
// Get gets a value from the StorageConfig, taking care about type checking.
// ptr must be a pointer or this method will panic.
// ptr will only be changed, if returned error != nil
diff --git a/gpg/gpg.go b/gpg/gpg.go
new file mode 100644
index 0000000..4639ae2
--- /dev/null
+++ b/gpg/gpg.go
@@ -0,0 +1,35 @@
+package gpg
+
+// Package gpg wraps around the gpg command line tool and exposes some of its functionality
+
+import (
+ "bytes"
+ "os/exec"
+)
+
+// Signer implements objects.Signer using gpg
+type Signer struct {
+ Key string
+}
+
+// Sign signs a message b with the key s.Key
+func (s Signer) Sign(b []byte) ([]byte, error) {
+ cmd := exec.Command("gpg", "--clearsign", "-u", s.Key)
+
+ cmd.Stdin = bytes.NewReader(b)
+ var out bytes.Buffer
+ cmd.Stdout = &out
+
+ err := cmd.Run()
+ return out.Bytes(), err
+}
+
+// Verifyer implements objects.Verifyer using gpg
+type Verifyer struct{}
+
+// Verify verifies the signed message b
+func (Verifyer) Verify(b []byte) error {
+ cmd := exec.Command("gpg", "--verify")
+ cmd.Stdin = bytes.NewReader(b)
+ return cmd.Run()
+}
diff --git a/main.go b/main.go
index b0f3f6b..36d469f 100644
--- a/main.go
+++ b/main.go
@@ -11,11 +11,12 @@ import (
type subcmd func(args []string) int
var subcmds = map[string]subcmd{
- "write-dir": WriteDir,
- "restore-dir": RestoreDir,
- "take-snapshot": notImplementedYet,
- "create-snapshot": notImplementedYet,
- "list-snapshots": notImplementedYet,
+ "write-dir": WriteDir,
+ "restore-dir": RestoreDir,
+ "take-snapshot": TakeSnapshot,
+ "create-snapshot": CreateSnapshot,
+ "list-snapshots": ListSnapshots,
+ "restore-snapshot": RestoreSnapshot,
}
func subcmdUsage(name string, usage string, flags *flag.FlagSet) func() {
@@ -28,6 +29,12 @@ func subcmdUsage(name string, usage string, flags *flag.FlagSet) func() {
}
}
+func subcmdErrout(name string) func(error) {
+ return func(err error) {
+ fmt.Fprintf(os.Stderr, "%s: %s\n", name, err)
+ }
+}
+
// Global flags
var (
flagConfPath = flag.String("config", "", "Use this config file instead of the default")
@@ -116,8 +123,3 @@ func loadConfig() bool {
objectstore = s
return true
}
-
-func notImplementedYet(_ []string) int {
- fmt.Fprintln(os.Stderr, "Not implemented yet")
- return 1
-}
diff --git a/objects/id.go b/objects/id.go
index 224b3cd..047fada 100644
--- a/objects/id.go
+++ b/objects/id.go
@@ -39,7 +39,7 @@ type ObjectId struct {
Sum []byte
}
-func (oid ObjectId) wellformed() bool {
+func (oid ObjectId) Wellformed() bool {
return oid.Algo.checkAlgo() && len(oid.Sum) == oid.Algo.sumLength()
}
@@ -61,13 +61,19 @@ func ParseObjectId(s string) (oid ObjectId, err error) {
return
}
- if !oid.wellformed() {
+ if !oid.Wellformed() {
err = errors.New("Object ID is malformed")
}
return
}
+// Set implements flag.Value for ObjectId
+func (oid *ObjectId) Set(s string) (err error) {
+ *oid, err = ParseObjectId(s)
+ return
+}
+
func MustParseObjectId(s string) ObjectId {
id, err := ParseObjectId(s)
if err != nil {
diff --git a/objects/object_snapshot.go b/objects/object_snapshot.go
index 7edbc2a..e86484d 100644
--- a/objects/object_snapshot.go
+++ b/objects/object_snapshot.go
@@ -23,6 +23,7 @@ type Snapshot struct {
Archive string
Comment string
Signed bool
+ raw []byte
}
func (s Snapshot) Type() ObjectType {
@@ -62,6 +63,20 @@ func (s Snapshot) Payload() (out []byte) {
return out
}
+type Verifyer interface {
+ Verify([]byte) error
+}
+
+// Verify verifies that the snapshot has a valid signature.
+// Only works with unserialized snapshots, i.e. a freshly created snapshot can not be verified.
+func (s Snapshot) Verify(v Verifyer) error {
+ if !s.Signed {
+ return nil
+ }
+
+ return v.Verify(s.raw)
+}
+
type Signer interface {
Sign([]byte) ([]byte, error)
}
@@ -73,6 +88,8 @@ func (s Snapshot) SignedPayload(signer Signer) ([]byte, error) {
func (s *Snapshot) FromPayload(payload []byte) error {
r := bytes.NewBuffer(payload)
+ s.raw = payload
+
seenArchive := false
seenDate := false
seenTree := false
diff --git a/restore_dir.go b/restore_dir.go
index 65f2785..2961b70 100644
--- a/restore_dir.go
+++ b/restore_dir.go
@@ -5,11 +5,11 @@ import (
"code.laria.me/petrific/fs"
"code.laria.me/petrific/objects"
"fmt"
- "os"
)
func RestoreDir(args []string) int {
usage := subcmdUsage("restore-dir", "directory object-id", nil)
+ errout := subcmdErrout("restore-dir")
if len(args) != 2 {
usage()
@@ -18,29 +18,29 @@ func RestoreDir(args []string) int {
dir_path, err := abspath(args[0])
if err != nil {
- fmt.Fprintf(os.Stderr, "restore-dir: %s\n", err)
+ errout(err)
return 1
}
d, err := fs.OpenOSFile(dir_path)
if err != nil {
- fmt.Fprintf(os.Stderr, "restore-dir: %s\n", err)
+ errout(err)
return 1
}
if d.Type() != fs.FDir {
- fmt.Fprintf(os.Stderr, "restore-dir: %s is not a directory\n", dir_path)
+ errout(fmt.Errorf("%s is not a directory", dir_path))
return 1
}
id, err := objects.ParseObjectId(args[1])
if err != nil {
- fmt.Fprintf(os.Stderr, "restore-dir: %s\n", err)
+ errout(err)
return 1
}
if err := backup.RestoreDir(objectstore, id, d); err != nil {
- fmt.Fprintf(os.Stderr, "restore-dir: %s\n", err)
+ errout(err)
return 1
}
diff --git a/snapshot.go b/snapshot.go
new file mode 100644
index 0000000..844a85a
--- /dev/null
+++ b/snapshot.go
@@ -0,0 +1,275 @@
+package main
+
+import (
+ "code.laria.me/petrific/backup"
+ "code.laria.me/petrific/cache"
+ "code.laria.me/petrific/fs"
+ "code.laria.me/petrific/gpg"
+ "code.laria.me/petrific/objects"
+ "code.laria.me/petrific/storage"
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "sort"
+ "time"
+)
+
+func createSnapshot(archive, comment string, tree_id objects.ObjectId, nosign bool) (objects.ObjectId, error) {
+ snapshot := objects.Snapshot{
+ Archive: archive,
+ Comment: comment,
+ Date: time.Now(),
+ Tree: tree_id,
+ Signed: !nosign,
+ }
+
+ var payload []byte
+ if nosign {
+ payload = snapshot.Payload()
+ } else {
+ var err error
+ payload, err = snapshot.SignedPayload(conf.GPGSigner())
+ if err != nil {
+ return objects.ObjectId{}, fmt.Errorf("could not sign: %s", err)
+ }
+ }
+
+ obj := objects.RawObject{
+ Type: objects.OTSnapshot,
+ Payload: payload,
+ }
+
+ return storage.SetObject(objectstore, obj)
+}
+
+func CreateSnapshot(args []string) int {
+ flags := flag.NewFlagSet(os.Args[0]+" create-snapshot", flag.ContinueOnError)
+ nosign := flags.Bool("nosign", false, "don't sign the snapshot (not recommended)")
+ comment := flags.String("comment", "", "comment for the snapshot")
+
+ flags.Usage = subcmdUsage("create-snapshot", "[flags] archive tree-object", flags)
+ errout := subcmdErrout("create-snapshot")
+
+ err := flags.Parse(args)
+ if err != nil {
+ errout(err)
+ return 2
+ }
+
+ args = flags.Args()
+ if len(args) != 2 {
+ flags.Usage()
+ return 2
+ }
+
+ tree_id, err := objects.ParseObjectId(args[1])
+ if err != nil {
+ errout(fmt.Errorf("invalid tree id: %s\n", err))
+ return 1
+ }
+
+ snapshot_id, err := createSnapshot(args[0], *comment, tree_id, *nosign)
+ if err != nil {
+ errout(err)
+ return 1
+ }
+
+ fmt.Println(snapshot_id)
+ return 0
+}
+
+func TakeSnapshot(args []string) int {
+ flags := flag.NewFlagSet(os.Args[0]+" take-snapshot", flag.ContinueOnError)
+ nosign := flags.Bool("nosign", false, "don't sign the snapshot (not recommended)")
+ comment := flags.String("comment", "", "comment for the snapshot")
+
+ flags.Usage = subcmdUsage("take-snapshot", "[flags] archive dir", flags)
+ errout := subcmdErrout("take-snapshot")
+
+ if err := flags.Parse(args); err != nil {
+ errout(err)
+ return 2
+ }
+
+ args = flags.Args()
+ if len(args) != 2 {
+ flags.Usage()
+ return 2
+ }
+
+ dir_path, err := abspath(args[1])
+ if err != nil {
+ errout(err)
+ return 1
+ }
+
+ d, err := fs.OpenOSFile(dir_path)
+ if err != nil {
+ errout(err)
+ return 1
+ }
+
+ if d.Type() != fs.FDir {
+ errout(fmt.Errorf("%s is not a directory\n", dir_path))
+ return 1
+ }
+
+ tree_id, err := backup.WriteDir(objectstore, dir_path, d, cache.NopCache{})
+ if err != nil {
+ errout(err)
+ return 1
+ }
+
+ snapshot_id, err := createSnapshot(args[0], *comment, tree_id, *nosign)
+ if err != nil {
+ errout(err)
+ fmt.Fprintf(os.Stderr, "You can try again by running `%s create-snapshot -c '%s' '%s' '%s'\n`", os.Args[0], *comment, args[0], tree_id)
+ return 1
+ }
+
+ fmt.Println(snapshot_id)
+ return 0
+}
+
+type snapshotWithId struct {
+ id objects.ObjectId
+ snapshot objects.Snapshot
+}
+
+type sortableSnapshots []snapshotWithId
+
+func (s sortableSnapshots) Len() int { return len(s) }
+func (s sortableSnapshots) Less(i, j int) bool { return s[i].snapshot.Date.After(s[j].snapshot.Date) }
+func (s sortableSnapshots) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+func ListSnapshots(args []string) int {
+ // usage := subcmdUsage("list-snapshots", "[archive]", nil)
+ errout := subcmdErrout("list-snapshots")
+
+ filter := func(s objects.Snapshot) bool { return true }
+ if len(args) > 0 {
+ archive := args[1]
+ filter = func(s objects.Snapshot) bool {
+ return s.Archive == archive
+ }
+ }
+
+ objids, err := objectstore.List(objects.OTSnapshot)
+ if err != nil {
+ errout(err)
+ return 1
+ }
+
+ snapshots := make(sortableSnapshots, 0)
+
+ failed := false
+ for _, objid := range objids {
+ _snapshot, err := storage.GetObjectOfType(objectstore, objid, objects.OTSnapshot)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "warning: list-snapshots: could not get snapshot %s: %s\n", objid, err)
+ failed = true
+ continue
+ }
+
+ snapshot := *_snapshot.(*objects.Snapshot)
+
+ if !filter(snapshot) {
+ continue
+ }
+
+ snapshots = append(snapshots, snapshotWithId{objid, snapshot})
+ }
+
+ sort.Sort(snapshots)
+
+ for _, snapshot_id := range snapshots {
+ fmt.Printf("%s\t%s\t%s\n\t%s\n", snapshot_id.snapshot.Archive, snapshot_id.snapshot.Date, snapshot_id.id, snapshot_id.snapshot.Comment)
+ }
+
+ if failed {
+ return 1
+ }
+ return 0
+}
+
+func RestoreSnapshot(args []string) int {
+ var snapshotId objects.ObjectId
+ flags := flag.NewFlagSet("restore-snapshot", flag.ContinueOnError)
+ flags.Var(&snapshotId, "id", "Object id of a snapshot")
+ archive := flags.String("archive", "", "Get latest snapshot for this archive")
+
+ flags.Usage = subcmdUsage("restore-snapshot", "[flags] directory", flags)
+ errout := subcmdErrout("restore-snapshot")
+
+ err := flags.Parse(args)
+ if err != nil {
+ errout(err)
+ return 2
+ }
+
+ args = flags.Args()
+ if len(args) < 1 {
+ flags.Usage()
+ return 2
+ }
+
+ if !snapshotId.Wellformed() && *archive == "" {
+ errout(errors.New("Either -id or -archive must be given"))
+ flags.Usage()
+ return 2
+ }
+
+ dir_path, err := abspath(args[0])
+ if err != nil {
+ errout(err)
+ return 1
+ }
+
+ _, err = os.Stat(dir_path)
+ if err != nil && os.IsNotExist(err) {
+ if err := os.MkdirAll(dir_path, 0755); err != nil {
+ errout(err)
+ return 1
+ }
+ }
+
+ root, err := fs.OpenOSFile(dir_path)
+ if err != nil {
+ errout(err)
+ return 1
+ }
+
+ if root.Type() != fs.FDir {
+ errout(fmt.Errorf("%s is not a directory\n", dir_path))
+ return 1
+ }
+
+ var snapshot *objects.Snapshot
+
+ if *archive != "" {
+ snapshot, err = storage.FindLatestSnapshot(objectstore, *archive)
+ if err != nil {
+ errout(err)
+ return 1
+ }
+ } else {
+ _snapshot, err := storage.GetObjectOfType(objectstore, snapshotId, objects.OTSnapshot)
+ if err != nil {
+ errout(err)
+ return 1
+ }
+ snapshot = _snapshot.(*objects.Snapshot)
+ }
+
+ if err := snapshot.Verify(gpg.Verifyer{}); err != nil {
+ errout(fmt.Errorf("verification failed: %s", err))
+ return 1
+ }
+
+ if err := backup.RestoreDir(objectstore, snapshot.Tree, root); err != nil {
+ errout(err)
+ return 1
+ }
+ return 0
+}
diff --git a/storage/storage.go b/storage/storage.go
index 7cffe9c..7d09a70 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
+ "time"
)
var (
@@ -89,3 +90,39 @@ func GetObjectOfType(s Storage, id objects.ObjectId, t objects.ObjectType) (obje
return rawobj.Object()
}
+
+// FindLatestSnapshot finds the latest snapshot, optionally filtered by archive
+func FindLatestSnapshot(store Storage, archive string) (latestSnapshot *objects.Snapshot, err error) {
+ ids, err := store.List(objects.OTSnapshot)
+ if err != nil {
+ return nil, err
+ }
+
+ var earliestTime time.Time
+ found := false
+
+ for _, id := range ids {
+ _snapshot, err := GetObjectOfType(store, id, objects.OTSnapshot)
+ if err != nil {
+ return nil, err
+ }
+
+ snapshot := _snapshot.(*objects.Snapshot)
+
+ if archive != "" && snapshot.Archive != archive {
+ continue
+ }
+
+ if snapshot.Date.After(earliestTime) {
+ earliestTime = snapshot.Date
+ latestSnapshot = snapshot
+ found = true
+ }
+ }
+
+ if !found {
+ return latestSnapshot, ObjectNotFound
+ }
+
+ return latestSnapshot, nil
+}
diff --git a/write_dir.go b/write_dir.go
index e701a56..34abcd1 100644
--- a/write_dir.go
+++ b/write_dir.go
@@ -22,6 +22,7 @@ func abspath(p string) (string, error) {
func WriteDir(args []string) int {
usage := subcmdUsage("write-dir", "directory", nil)
+ errout := subcmdErrout("write-dir")
if len(args) != 1 {
usage()
@@ -30,24 +31,24 @@ func WriteDir(args []string) int {
dir_path, err := abspath(args[0])
if err != nil {
- fmt.Fprintf(os.Stderr, "write-dir: %s\n", err)
+ errout(err)
return 1
}
d, err := fs.OpenOSFile(dir_path)
if err != nil {
- fmt.Fprintf(os.Stderr, "write-dir: %s\n", err)
+ errout(err)
return 1
}
if d.Type() != fs.FDir {
- fmt.Fprintf(os.Stderr, "write-dir: %s is not a directory\n", dir_path)
+ errout(fmt.Errorf("%s is not a directory\n", dir_path))
return 1
}
id, err := backup.WriteDir(objectstore, dir_path, d, cache.NopCache{})
if err != nil {
- fmt.Fprintf(os.Stderr, "write-dir: %s\n", err)
+ errout(err)
return 1
}