diff options
-rw-r--r-- | config/config.go | 5 | ||||
-rw-r--r-- | gpg/gpg.go | 35 | ||||
-rw-r--r-- | main.go | 22 | ||||
-rw-r--r-- | objects/id.go | 10 | ||||
-rw-r--r-- | objects/object_snapshot.go | 17 | ||||
-rw-r--r-- | restore_dir.go | 12 | ||||
-rw-r--r-- | snapshot.go | 275 | ||||
-rw-r--r-- | storage/storage.go | 37 | ||||
-rw-r--r-- | write_dir.go | 9 |
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() +} @@ -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 } |