forked from TrueCloudLab/restic
Add decrypt, refactor
This commit is contained in:
parent
83ea81d8c3
commit
30ab03b7b7
37 changed files with 2572 additions and 1046 deletions
189
archiver.go
Normal file
189
archiver.go
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
package khepri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Archiver struct {
|
||||||
|
be backend.Server
|
||||||
|
key *Key
|
||||||
|
ch *ContentHandler
|
||||||
|
smap *StorageMap // blobs used for the current snapshot
|
||||||
|
|
||||||
|
Error func(dir string, fi os.FileInfo, err error) error
|
||||||
|
Filter func(item string, fi os.FileInfo) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArchiver(be backend.Server, key *Key) (*Archiver, error) {
|
||||||
|
var err error
|
||||||
|
arch := &Archiver{be: be, key: key}
|
||||||
|
|
||||||
|
// abort on all errors
|
||||||
|
arch.Error = func(string, os.FileInfo, error) error { return err }
|
||||||
|
// allow all files
|
||||||
|
arch.Filter = func(string, os.FileInfo) bool { return true }
|
||||||
|
|
||||||
|
arch.smap = NewStorageMap()
|
||||||
|
arch.ch, err = NewContentHandler(be, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// load all blobs from all snapshots
|
||||||
|
err = arch.ch.LoadAllSnapshots()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return arch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arch *Archiver) Save(t backend.Type, data []byte) (*Blob, error) {
|
||||||
|
blob, err := arch.ch.Save(t, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// store blob in storage map for current snapshot
|
||||||
|
arch.smap.Insert(blob)
|
||||||
|
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arch *Archiver) SaveJSON(t backend.Type, item interface{}) (*Blob, error) {
|
||||||
|
blob, err := arch.ch.SaveJSON(t, item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// store blob in storage map for current snapshot
|
||||||
|
arch.smap.Insert(blob)
|
||||||
|
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arch *Archiver) SaveFile(node *Node) (Blobs, error) {
|
||||||
|
blobs, err := arch.ch.SaveFile(node.path, uint(node.Size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, arch.Error(node.path, nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Content = make([]backend.ID, len(blobs))
|
||||||
|
for i, blob := range blobs {
|
||||||
|
node.Content[i] = blob.ID
|
||||||
|
arch.smap.Insert(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blobs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arch *Archiver) ImportDir(dir string) (Tree, error) {
|
||||||
|
fd, err := os.Open(dir)
|
||||||
|
defer fd.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, arch.Error(dir, nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := fd.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, arch.Error(dir, nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tree := Tree{}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
path := filepath.Join(dir, entry.Name())
|
||||||
|
|
||||||
|
if !arch.Filter(path, entry) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := NodeFromFileInfo(path, entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, arch.Error(dir, entry, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = append(tree, node)
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
subtree, err := arch.ImportDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err := arch.SaveJSON(backend.Tree, subtree)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Subtree = blob.ID
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type == "file" {
|
||||||
|
_, err := arch.SaveFile(node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, arch.Error(path, entry, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arch *Archiver) Import(dir string) (*Snapshot, *Blob, error) {
|
||||||
|
sn := NewSnapshot(dir)
|
||||||
|
|
||||||
|
fi, err := os.Lstat(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := NodeFromFileInfo(dir, fi)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type == "dir" {
|
||||||
|
tree, err := arch.ImportDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err := arch.SaveJSON(backend.Tree, tree)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Subtree = blob.ID
|
||||||
|
} else if node.Type == "file" {
|
||||||
|
_, err := arch.SaveFile(node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err := arch.SaveJSON(backend.Tree, &Tree{node})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sn.Content = blob.ID
|
||||||
|
|
||||||
|
// save snapshot
|
||||||
|
sn.StorageMap = arch.smap
|
||||||
|
blob, err = arch.SaveJSON(backend.Snapshot, sn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sn, blob, nil
|
||||||
|
}
|
2
backend/doc.go
Normal file
2
backend/doc.go
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// Package backend provides local and remote storage for khepri backups.
|
||||||
|
package backend
|
84
backend/generic.go
Normal file
84
backend/generic.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"crypto/sha256"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Each lists all entries of type t in the backend and calls function f() with
|
||||||
|
// the id and data.
|
||||||
|
func Each(be Server, t Type, f func(id ID, data []byte, err error)) error {
|
||||||
|
ids, err := be.List(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
data, err := be.Get(t, id)
|
||||||
|
if err != nil {
|
||||||
|
f(id, nil, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f(id, data, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each lists all entries of type t in the backend and calls function f() with
|
||||||
|
// the id.
|
||||||
|
func EachID(be Server, t Type, f func(ID)) error {
|
||||||
|
ids, err := be.List(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
f(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress applies zlib compression to data.
|
||||||
|
func Compress(data []byte) []byte {
|
||||||
|
// apply zlib compression
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := zlib.NewWriter(&b)
|
||||||
|
_, err := w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncompress reverses zlib compression on data.
|
||||||
|
func Uncompress(data []byte) []byte {
|
||||||
|
b := bytes.NewBuffer(data)
|
||||||
|
r, err := zlib.NewReader(b)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Close()
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash returns the ID for data.
|
||||||
|
func Hash(data []byte) ID {
|
||||||
|
h := sha256.Sum256(data)
|
||||||
|
id := make(ID, 32)
|
||||||
|
copy(id, h[:])
|
||||||
|
return id
|
||||||
|
}
|
36
backend/generic_test.go
Normal file
36
backend/generic_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package backend_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// assert fails the test if the condition is false.
|
||||||
|
func assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
|
||||||
|
if !condition {
|
||||||
|
_, file, line, _ := runtime.Caller(1)
|
||||||
|
fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...)
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ok fails the test if an err is not nil.
|
||||||
|
func ok(tb testing.TB, err error) {
|
||||||
|
if err != nil {
|
||||||
|
_, file, line, _ := runtime.Caller(1)
|
||||||
|
fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error())
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// equals fails the test if exp is not equal to act.
|
||||||
|
func equals(tb testing.TB, exp, act interface{}) {
|
||||||
|
if !reflect.DeepEqual(exp, act) {
|
||||||
|
_, file, line, _ := runtime.Caller(1)
|
||||||
|
fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,15 @@
|
||||||
package khepri
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const sha256_length = 32 // in bytes
|
||||||
|
|
||||||
// References content within a repository.
|
// References content within a repository.
|
||||||
type ID []byte
|
type ID []byte
|
||||||
|
|
||||||
|
@ -18,6 +21,10 @@ func ParseID(s string) (ID, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(b) != sha256_length {
|
||||||
|
return nil, errors.New("invalid length for sha256 hash")
|
||||||
|
}
|
||||||
|
|
||||||
return ID(b), nil
|
return ID(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +69,37 @@ func (id *ID) UnmarshalJSON(b []byte) error {
|
||||||
|
|
||||||
func IDFromData(d []byte) ID {
|
func IDFromData(d []byte) ID {
|
||||||
hash := sha256.Sum256(d)
|
hash := sha256.Sum256(d)
|
||||||
id := make([]byte, 32)
|
id := make([]byte, sha256_length)
|
||||||
copy(id, hash[:])
|
copy(id, hash[:])
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IDs []ID
|
||||||
|
|
||||||
|
func (ids IDs) Len() int {
|
||||||
|
return len(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ids IDs) Less(i, j int) bool {
|
||||||
|
if len(ids[i]) < len(ids[j]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, b := range ids[i] {
|
||||||
|
if b == ids[j][k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b < ids[j][k] {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ids IDs) Swap(i, j int) {
|
||||||
|
ids[i], ids[j] = ids[j], ids[i]
|
||||||
|
}
|
21
backend/interface.go
Normal file
21
backend/interface.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Blob Type = "blob"
|
||||||
|
Key = "key"
|
||||||
|
Lock = "lock"
|
||||||
|
Snapshot = "snapshot"
|
||||||
|
Tree = "tree"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server interface {
|
||||||
|
Create(Type, []byte) (ID, error)
|
||||||
|
Get(Type, ID) ([]byte, error)
|
||||||
|
List(Type) (IDs, error)
|
||||||
|
Test(Type, ID) (bool, error)
|
||||||
|
Remove(Type, ID) error
|
||||||
|
|
||||||
|
Location() string
|
||||||
|
}
|
213
backend/local.go
Normal file
213
backend/local.go
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dirMode = 0700
|
||||||
|
blobPath = "blobs"
|
||||||
|
snapshotPath = "snapshots"
|
||||||
|
treePath = "trees"
|
||||||
|
lockPath = "locks"
|
||||||
|
keyPath = "keys"
|
||||||
|
tempPath = "tmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Local struct {
|
||||||
|
p string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenLocal opens the local backend at dir.
|
||||||
|
func OpenLocal(dir string) (*Local, error) {
|
||||||
|
items := []string{
|
||||||
|
dir,
|
||||||
|
filepath.Join(dir, blobPath),
|
||||||
|
filepath.Join(dir, snapshotPath),
|
||||||
|
filepath.Join(dir, treePath),
|
||||||
|
filepath.Join(dir, lockPath),
|
||||||
|
filepath.Join(dir, keyPath),
|
||||||
|
filepath.Join(dir, tempPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if all necessary dirs and files are there
|
||||||
|
for _, d := range items {
|
||||||
|
if _, err := os.Stat(d); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s does not exist", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Local{p: dir}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLocal creates all the necessary files and directories for a new local
|
||||||
|
// backend at dir.
|
||||||
|
func CreateLocal(dir string) (*Local, error) {
|
||||||
|
dirs := []string{
|
||||||
|
dir,
|
||||||
|
filepath.Join(dir, blobPath),
|
||||||
|
filepath.Join(dir, snapshotPath),
|
||||||
|
filepath.Join(dir, treePath),
|
||||||
|
filepath.Join(dir, lockPath),
|
||||||
|
filepath.Join(dir, keyPath),
|
||||||
|
filepath.Join(dir, tempPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if directories already exist
|
||||||
|
for _, d := range dirs[1:] {
|
||||||
|
if _, err := os.Stat(d); err == nil {
|
||||||
|
return nil, fmt.Errorf("dir %s already exists", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create paths for blobs, refs and temp
|
||||||
|
for _, d := range dirs {
|
||||||
|
err := os.MkdirAll(d, dirMode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// open repository
|
||||||
|
return OpenLocal(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns this backend's location (the directory name).
|
||||||
|
func (b *Local) Location() string {
|
||||||
|
return b.p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return temp directory in correct directory for this backend.
|
||||||
|
func (b *Local) tempFile() (*os.File, error) {
|
||||||
|
return ioutil.TempFile(filepath.Join(b.p, tempPath), "temp-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename temp file to final name according to type and ID.
|
||||||
|
func (b *Local) renameFile(file *os.File, t Type, id ID) error {
|
||||||
|
filename := filepath.Join(b.dir(t), id.String())
|
||||||
|
return os.Rename(file.Name(), filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct directory for given Type.
|
||||||
|
func (b *Local) dir(t Type) string {
|
||||||
|
var n string
|
||||||
|
switch t {
|
||||||
|
case Blob:
|
||||||
|
n = blobPath
|
||||||
|
case Snapshot:
|
||||||
|
n = snapshotPath
|
||||||
|
case Tree:
|
||||||
|
n = treePath
|
||||||
|
case Lock:
|
||||||
|
n = lockPath
|
||||||
|
case Key:
|
||||||
|
n = keyPath
|
||||||
|
}
|
||||||
|
return filepath.Join(b.p, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create stores new content of type t and data and returns the ID.
|
||||||
|
func (b *Local) Create(t Type, data []byte) (ID, error) {
|
||||||
|
// TODO: make sure that tempfile is removed upon error
|
||||||
|
|
||||||
|
// create tempfile in repository
|
||||||
|
var err error
|
||||||
|
file, err := b.tempFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write data to tempfile
|
||||||
|
_, err = file.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// close tempfile, return id
|
||||||
|
id := IDFromData(data)
|
||||||
|
err = b.renameFile(file, t, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct path for given Type and ID.
|
||||||
|
func (b *Local) filename(t Type, id ID) string {
|
||||||
|
return filepath.Join(b.dir(t), id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the content stored under the given ID.
|
||||||
|
func (b *Local) Get(t Type, id ID) ([]byte, error) {
|
||||||
|
// try to open file
|
||||||
|
file, err := os.Open(b.filename(t, id))
|
||||||
|
defer file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// read all
|
||||||
|
buf, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test returns true if a blob of the given type and ID exists in the backend.
|
||||||
|
func (b *Local) Test(t Type, id ID) (bool, error) {
|
||||||
|
// try to open file
|
||||||
|
file, err := os.Open(b.filename(t, id))
|
||||||
|
defer func() {
|
||||||
|
file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the content stored at ID.
|
||||||
|
func (b *Local) Remove(t Type, id ID) error {
|
||||||
|
return os.Remove(b.filename(t, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists all objects of a given type.
|
||||||
|
func (b *Local) List(t Type) (IDs, error) {
|
||||||
|
// TODO: use os.Open() and d.Readdirnames() instead of Glob()
|
||||||
|
pattern := filepath.Join(b.dir(t), "*")
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make(IDs, 0, len(matches))
|
||||||
|
|
||||||
|
for _, m := range matches {
|
||||||
|
base := filepath.Base(m)
|
||||||
|
|
||||||
|
if base == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, err := ParseID(base)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
171
backend/local_test.go
Normal file
171
backend/local_test.go
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
package backend_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testCleanup = flag.Bool("test.cleanup", true, "clean up after running tests (remove local backend directory with all content)")
|
||||||
|
|
||||||
|
var TestStrings = []struct {
|
||||||
|
id string
|
||||||
|
data string
|
||||||
|
}{
|
||||||
|
{"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", "foobar"},
|
||||||
|
{"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"},
|
||||||
|
{"cc5d46bdb4991c6eae3eb739c9c8a7a46fe9654fab79c47b4fe48383b5b25e1c", "foo/bar"},
|
||||||
|
{"4e54d2c721cbdb730f01b10b62dec622962b36966ec685880effa63d71c808f2", "foo/../../baz"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupBackend(t *testing.T) *backend.Local {
|
||||||
|
tempdir, err := ioutil.TempDir("", "khepri-test-")
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
b, err := backend.CreateLocal(tempdir)
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
t.Logf("created local backend at %s", tempdir)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardownBackend(t *testing.T, b *backend.Local) {
|
||||||
|
if !*testCleanup {
|
||||||
|
t.Logf("leaving local backend at %s\n", b.Location())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(t, os.RemoveAll(b.Location()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBackend(b backend.Server, t *testing.T) {
|
||||||
|
for _, tpe := range []backend.Type{backend.Blob, backend.Key, backend.Lock, backend.Snapshot, backend.Tree} {
|
||||||
|
// detect non-existing files
|
||||||
|
for _, test := range TestStrings {
|
||||||
|
id, err := backend.ParseID(test.id)
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
// test if blob is already in repository
|
||||||
|
ret, err := b.Test(tpe, id)
|
||||||
|
ok(t, err)
|
||||||
|
assert(t, !ret, "blob was found to exist before creating")
|
||||||
|
|
||||||
|
// try to open not existing blob
|
||||||
|
d, err := b.Get(tpe, id)
|
||||||
|
assert(t, err != nil && d == nil, "blob data could be extracted befor creation")
|
||||||
|
|
||||||
|
// try to get string out, should fail
|
||||||
|
ret, err = b.Test(tpe, id)
|
||||||
|
ok(t, err)
|
||||||
|
assert(t, !ret, fmt.Sprintf("id %q was found (but should not have)", test.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// add files
|
||||||
|
for _, test := range TestStrings {
|
||||||
|
// store string in backend
|
||||||
|
id, err := b.Create(tpe, []byte(test.data))
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
equals(t, test.id, id.String())
|
||||||
|
|
||||||
|
// try to get it out again
|
||||||
|
buf, err := b.Get(tpe, id)
|
||||||
|
ok(t, err)
|
||||||
|
assert(t, buf != nil, "Get() returned nil")
|
||||||
|
|
||||||
|
// compare content
|
||||||
|
equals(t, test.data, string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// list items
|
||||||
|
IDs := backend.IDs{}
|
||||||
|
|
||||||
|
for _, test := range TestStrings {
|
||||||
|
id, err := backend.ParseID(test.id)
|
||||||
|
ok(t, err)
|
||||||
|
IDs = append(IDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := b.List(tpe)
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
sort.Sort(ids)
|
||||||
|
sort.Sort(IDs)
|
||||||
|
equals(t, IDs, ids)
|
||||||
|
|
||||||
|
// remove content if requested
|
||||||
|
if *testCleanup {
|
||||||
|
for _, test := range TestStrings {
|
||||||
|
id, err := backend.ParseID(test.id)
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
found, err := b.Test(tpe, id)
|
||||||
|
ok(t, err)
|
||||||
|
assert(t, found, fmt.Sprintf("id %q was not found before removal"))
|
||||||
|
|
||||||
|
ok(t, b.Remove(tpe, id))
|
||||||
|
|
||||||
|
found, err = b.Test(tpe, id)
|
||||||
|
ok(t, err)
|
||||||
|
assert(t, !found, fmt.Sprintf("id %q was not found before removal"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend(t *testing.T) {
|
||||||
|
// test for non-existing backend
|
||||||
|
b, err := backend.OpenLocal("/invalid-khepri-test")
|
||||||
|
assert(t, err != nil, "opening invalid repository at /invalid-khepri-test should have failed, but err is nil")
|
||||||
|
assert(t, b == nil, fmt.Sprintf("opening invalid repository at /invalid-khepri-test should have failed, but b is not nil: %v", b))
|
||||||
|
|
||||||
|
b = setupBackend(t)
|
||||||
|
defer teardownBackend(t, b)
|
||||||
|
|
||||||
|
testBackend(b, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalBackendCreationFailures(t *testing.T) {
|
||||||
|
b := setupBackend(t)
|
||||||
|
defer teardownBackend(t, b)
|
||||||
|
|
||||||
|
// test failure to create a new repository at the same location
|
||||||
|
b2, err := backend.CreateLocal(b.Location())
|
||||||
|
assert(t, err != nil && b2 == nil, fmt.Sprintf("creating a repository at %s for the second time should have failed", b.Location()))
|
||||||
|
|
||||||
|
// test failure to create a new repository at the same location without a config file
|
||||||
|
b2, err = backend.CreateLocal(b.Location())
|
||||||
|
assert(t, err != nil && b2 == nil, fmt.Sprintf("creating a repository at %s for the second time should have failed", b.Location()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestID(t *testing.T) {
|
||||||
|
for _, test := range TestStrings {
|
||||||
|
id, err := backend.ParseID(test.id)
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
id2, err := backend.ParseID(test.id)
|
||||||
|
ok(t, err)
|
||||||
|
assert(t, id.Equal(id2), "ID.Equal() does not work as expected")
|
||||||
|
|
||||||
|
ret, err := id.EqualString(test.id)
|
||||||
|
ok(t, err)
|
||||||
|
assert(t, ret, "ID.EqualString() returned wrong value")
|
||||||
|
|
||||||
|
// test json marshalling
|
||||||
|
buf, err := id.MarshalJSON()
|
||||||
|
ok(t, err)
|
||||||
|
equals(t, "\""+test.id+"\"", string(buf))
|
||||||
|
|
||||||
|
var id3 backend.ID
|
||||||
|
err = id3.UnmarshalJSON(buf)
|
||||||
|
ok(t, err)
|
||||||
|
equals(t, id, id3)
|
||||||
|
}
|
||||||
|
}
|
81
cmd/archive/main.go
Normal file
81
cmd/archive/main.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pass = "foobar"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: archive REPO DIR\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
repo := os.Args[1]
|
||||||
|
dir := os.Args[2]
|
||||||
|
|
||||||
|
// fmt.Printf("import %s into backend %s\n", dir, repo)
|
||||||
|
|
||||||
|
var (
|
||||||
|
be backend.Server
|
||||||
|
key *khepri.Key
|
||||||
|
)
|
||||||
|
|
||||||
|
be, err := backend.OpenLocal(repo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("creating %s\n", repo)
|
||||||
|
be, err = backend.CreateLocal(repo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err = khepri.CreateKey(be, pass)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err = khepri.SearchKey(be, pass)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
arch, err := khepri.NewArchiver(be, key)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "err: %v\n", err)
|
||||||
|
}
|
||||||
|
arch.Error = func(dir string, fi os.FileInfo, err error) error {
|
||||||
|
fmt.Fprintf(os.Stderr, "error for %s: %v\n%s\n", dir, err, fi)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
arch.Filter = func(item string, fi os.FileInfo) bool {
|
||||||
|
// if fi.IsDir() {
|
||||||
|
// if fi.Name() == ".svn" {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// if filepath.Ext(fi.Name()) == ".bz2" {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fmt.Printf("%s\n", item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blob, err := arch.Import(dir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Import() error: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("saved as %+v\n", blob)
|
||||||
|
}
|
126
cmd/cat/main.go
Normal file
126
cmd/cat/main.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.google.com/p/go.crypto/ssh/terminal"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
func read_password(prompt string) string {
|
||||||
|
p := os.Getenv("KHEPRI_PASSWORD")
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(prompt)
|
||||||
|
pw, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "unable to read password: %v", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return string(pw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func json_pp(data []byte) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := json.Indent(&buf, data, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(buf.Bytes()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StopWatch struct {
|
||||||
|
start, last time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStopWatch() *StopWatch {
|
||||||
|
return &StopWatch{
|
||||||
|
start: time.Now(),
|
||||||
|
last: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StopWatch) Next(format string, data ...interface{}) {
|
||||||
|
t := time.Now()
|
||||||
|
d := t.Sub(s.last)
|
||||||
|
s.last = t
|
||||||
|
arg := make([]interface{}, len(data)+1)
|
||||||
|
arg[0] = d
|
||||||
|
copy(arg[1:], data)
|
||||||
|
fmt.Printf("[%s]: "+format+"\n", arg...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: cat REPO ID\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
repo := os.Args[1]
|
||||||
|
id, err := backend.ParseID(filepath.Base(os.Args[2]))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := NewStopWatch()
|
||||||
|
|
||||||
|
be, err := backend.OpenLocal(repo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Next("OpenLocal()")
|
||||||
|
|
||||||
|
key, err := khepri.SearchKey(be, read_password("Enter Password for Repository: "))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Next("SearchKey()")
|
||||||
|
|
||||||
|
// try all possible types
|
||||||
|
for _, t := range []backend.Type{backend.Blob, backend.Snapshot, backend.Lock, backend.Tree, backend.Key} {
|
||||||
|
buf, err := be.Get(t, id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Next("Get(%s, %s)", t, id)
|
||||||
|
|
||||||
|
if t == backend.Key {
|
||||||
|
json_pp(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf2, err := key.Decrypt(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t == backend.Blob {
|
||||||
|
// directly output blob
|
||||||
|
fmt.Println(string(buf2))
|
||||||
|
} else {
|
||||||
|
// try to uncompress and print as idented json
|
||||||
|
err = json_pp(backend.Uncompress(buf2))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
233
cmd/decrypt/main.go
Normal file
233
cmd/decrypt/main.go
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
|
||||||
|
"code.google.com/p/go.crypto/scrypt"
|
||||||
|
"code.google.com/p/go.crypto/ssh/terminal"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
scrypt_N = 65536
|
||||||
|
scrypt_r = 8
|
||||||
|
scrypt_p = 1
|
||||||
|
aesKeySize = 32 // for AES256
|
||||||
|
)
|
||||||
|
|
||||||
|
var Opts struct {
|
||||||
|
Password string `short:"p" long:"password" description:"Password for the file"`
|
||||||
|
Keys string `short:"k" long:"keys" description:"Keys for the file (encryption_key || sign_key, hex-encoded)"`
|
||||||
|
Salt string `short:"s" long:"salt" description:"Salt to use (hex-encoded)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIV() ([]byte, error) {
|
||||||
|
buf := make([]byte, aes.BlockSize)
|
||||||
|
_, err := io.ReadFull(rand.Reader, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pad(plaintext []byte) []byte {
|
||||||
|
l := aes.BlockSize - (len(plaintext) % aes.BlockSize)
|
||||||
|
if l == 0 {
|
||||||
|
l = aes.BlockSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if l <= 0 || l > aes.BlockSize {
|
||||||
|
panic("invalid padding size")
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(plaintext, bytes.Repeat([]byte{byte(l)}, l)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpad(plaintext []byte) []byte {
|
||||||
|
l := len(plaintext)
|
||||||
|
pad := plaintext[l-1]
|
||||||
|
|
||||||
|
if pad > aes.BlockSize {
|
||||||
|
panic(errors.New("padding > BlockSize"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if pad == 0 {
|
||||||
|
panic(errors.New("invalid padding 0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := l - int(pad); i < l; i++ {
|
||||||
|
if plaintext[i] != pad {
|
||||||
|
panic(errors.New("invalid padding!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext[:l-int(pad)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts and signs data. Returned is IV || Ciphertext || HMAC. For
|
||||||
|
// the hash function, SHA256 is used, so the overhead is 16+32=48 byte.
|
||||||
|
func Encrypt(ekey, skey []byte, plaintext []byte) ([]byte, error) {
|
||||||
|
iv, err := newIV()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to generate new random iv: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := aes.NewCipher(ekey)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to create cipher: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
e := cipher.NewCBCEncrypter(c, iv)
|
||||||
|
p := pad(plaintext)
|
||||||
|
ciphertext := make([]byte, len(p))
|
||||||
|
e.CryptBlocks(ciphertext, p)
|
||||||
|
|
||||||
|
ciphertext = append(iv, ciphertext...)
|
||||||
|
|
||||||
|
hm := hmac.New(sha256.New, skey)
|
||||||
|
|
||||||
|
n, err := hm.Write(ciphertext)
|
||||||
|
if err != nil || n != len(ciphertext) {
|
||||||
|
panic(fmt.Sprintf("unable to calculate hmac of ciphertext: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return hm.Sum(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt verifes and decrypts the ciphertext. Ciphertext must be in the form
|
||||||
|
// IV || Ciphertext || HMAC.
|
||||||
|
func Decrypt(ekey, skey []byte, ciphertext []byte) ([]byte, error) {
|
||||||
|
hm := hmac.New(sha256.New, skey)
|
||||||
|
|
||||||
|
// extract hmac
|
||||||
|
l := len(ciphertext) - hm.Size()
|
||||||
|
ciphertext, mac := ciphertext[:l], ciphertext[l:]
|
||||||
|
|
||||||
|
// calculate new hmac
|
||||||
|
n, err := hm.Write(ciphertext)
|
||||||
|
if err != nil || n != len(ciphertext) {
|
||||||
|
panic(fmt.Sprintf("unable to calculate hmac of ciphertext, err %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify hmac
|
||||||
|
mac2 := hm.Sum(nil)
|
||||||
|
if !hmac.Equal(mac, mac2) {
|
||||||
|
panic("HMAC verification failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract iv
|
||||||
|
iv, ciphertext := ciphertext[:aes.BlockSize], ciphertext[aes.BlockSize:]
|
||||||
|
|
||||||
|
// decrypt data
|
||||||
|
c, err := aes.NewCipher(ekey)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to create cipher: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt
|
||||||
|
e := cipher.NewCBCDecrypter(c, iv)
|
||||||
|
plaintext := make([]byte, len(ciphertext))
|
||||||
|
e.CryptBlocks(plaintext, ciphertext)
|
||||||
|
|
||||||
|
// remove padding and return
|
||||||
|
return unpad(plaintext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func errx(code int, format string, data ...interface{}) {
|
||||||
|
if len(format) > 0 && format[len(format)-1] != '\n' {
|
||||||
|
format += "\n"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, format, data...)
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func read_password(prompt string) string {
|
||||||
|
p := os.Getenv("KHEPRI_PASSWORD")
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(prompt)
|
||||||
|
pw, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
errx(2, "unable to read password: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return string(pw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args, err := flags.Parse(&Opts)
|
||||||
|
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []byte
|
||||||
|
|
||||||
|
if Opts.Password == "" && Opts.Keys == "" {
|
||||||
|
Opts.Password = read_password("password: ")
|
||||||
|
|
||||||
|
salt, err := hex.DecodeString(Opts.Salt)
|
||||||
|
if err != nil {
|
||||||
|
errx(1, "unable to hex-decode salt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err = scrypt.Key([]byte(Opts.Password), salt, scrypt_N, scrypt_r, scrypt_p, 2*aesKeySize)
|
||||||
|
if err != nil {
|
||||||
|
errx(1, "scrypt: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Opts.Keys != "" {
|
||||||
|
keys, err = hex.DecodeString(Opts.Keys)
|
||||||
|
if err != nil {
|
||||||
|
errx(1, "unable to hex-decode keys: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) != 2*aesKeySize {
|
||||||
|
errx(2, "key length is not 512")
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt_key := keys[:aesKeySize]
|
||||||
|
sign_key := keys[aesKeySize:]
|
||||||
|
|
||||||
|
for _, filename := range args {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
defer f.Close()
|
||||||
|
if err != nil {
|
||||||
|
errx(3, "%v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
errx(3, "%v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err = Decrypt(encrypt_key, sign_key, buf)
|
||||||
|
if err != nil {
|
||||||
|
errx(3, "%v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stdout.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
errx(3, "%v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -53,14 +53,17 @@ func walk(dir string) <-chan *entry {
|
||||||
|
|
||||||
func (e *entry) equals(other *entry) bool {
|
func (e *entry) equals(other *entry) bool {
|
||||||
if e.path != other.path {
|
if e.path != other.path {
|
||||||
|
fmt.Printf("path does not match\n")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.fi.Mode() != other.fi.Mode() {
|
if e.fi.Mode() != other.fi.Mode() {
|
||||||
|
fmt.Printf("mode does not match\n")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.fi.ModTime() != other.fi.ModTime() {
|
if e.fi.ModTime() != other.fi.ModTime() {
|
||||||
|
fmt.Printf("ModTime does not match\n")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,37 +3,34 @@ package main
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"os"
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commandBackup(repo *khepri.Repository, args []string) error {
|
func commandBackup(be backend.Server, key *khepri.Key, args []string) error {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return errors.New("usage: backup dir")
|
return errors.New("usage: backup [dir|file]")
|
||||||
}
|
}
|
||||||
|
|
||||||
target := args[0]
|
target := args[0]
|
||||||
|
|
||||||
tree, err := khepri.NewTreeFromPath(repo, target)
|
arch, err := khepri.NewArchiver(be, key)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "err: %v\n", err)
|
||||||
|
}
|
||||||
|
arch.Error = func(dir string, fi os.FileInfo, err error) error {
|
||||||
|
fmt.Fprintf(os.Stderr, "error for %s: %v\n%s\n", dir, err, fi)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blob, err := arch.Import(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := tree.Save(repo)
|
fmt.Printf("snapshot %s saved\n", blob.Storage)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sn := khepri.NewSnapshot(target)
|
|
||||||
sn.Content = id
|
|
||||||
snid, err := sn.Save(repo)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error saving snapshopt: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%q archived as %v\n", target, snid)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dump_tree(repo *khepri.Repository, id khepri.ID) error {
|
|
||||||
tree, err := khepri.NewTreeFromRepo(repo, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := json.MarshalIndent(tree, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("tree %s\n%s\n", id, buf)
|
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
|
||||||
if node.Type == "dir" {
|
|
||||||
err = dump_tree(repo, node.Subtree)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dump_snapshot(repo *khepri.Repository, id khepri.ID) error {
|
|
||||||
sn, err := khepri.LoadSnapshot(repo, id)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error loading snapshot %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := json.MarshalIndent(sn, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s\n%s\n", sn, buf)
|
|
||||||
|
|
||||||
return dump_tree(repo, sn.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dump_file(repo *khepri.Repository, id khepri.ID) error {
|
|
||||||
rd, err := repo.Get(khepri.TYPE_BLOB, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
io.Copy(os.Stdout, rd)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandDump(repo *khepri.Repository, args []string) error {
|
|
||||||
if len(args) != 2 {
|
|
||||||
return errors.New("usage: dump [snapshot|tree|file] ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
tpe := args[0]
|
|
||||||
|
|
||||||
id, err := khepri.ParseID(args[1])
|
|
||||||
if err != nil {
|
|
||||||
errx(1, "invalid id %q: %v", args[0], err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tpe {
|
|
||||||
case "snapshot":
|
|
||||||
return dump_snapshot(repo, id)
|
|
||||||
case "tree":
|
|
||||||
return dump_tree(repo, id)
|
|
||||||
case "file":
|
|
||||||
return dump_file(repo, id)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid type %q", tpe)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,84 +1,76 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/fd0/khepri/backend"
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
// func fsck_tree(be backend.Server, id backend.ID) (bool, error) {
|
||||||
)
|
// log.Printf(" checking dir %s", id)
|
||||||
|
|
||||||
func fsck_tree(repo *khepri.Repository, id khepri.ID) (bool, error) {
|
// buf, err := be.GetBlob(id)
|
||||||
log.Printf(" checking dir %s", id)
|
// if err != nil {
|
||||||
|
// return false, err
|
||||||
|
// }
|
||||||
|
|
||||||
rd, err := repo.Get(khepri.TYPE_BLOB, id)
|
// tree := &khepri.Tree{}
|
||||||
if err != nil {
|
// err = json.Unmarshal(buf, tree)
|
||||||
return false, err
|
// if err != nil {
|
||||||
}
|
// return false, err
|
||||||
|
// }
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(rd)
|
// if !id.Equal(backend.IDFromData(buf)) {
|
||||||
|
// return false, nil
|
||||||
|
// }
|
||||||
|
|
||||||
tree := &khepri.Tree{}
|
// return true, nil
|
||||||
err = json.Unmarshal(buf, tree)
|
// }
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !id.Equal(khepri.IDFromData(buf)) {
|
// func fsck_snapshot(be backend.Server, id backend.ID) (bool, error) {
|
||||||
return false, nil
|
// log.Printf("checking snapshot %s", id)
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
// sn, err := khepri.LoadSnapshot(be, id)
|
||||||
}
|
// if err != nil {
|
||||||
|
// return false, err
|
||||||
|
// }
|
||||||
|
|
||||||
func fsck_snapshot(repo *khepri.Repository, id khepri.ID) (bool, error) {
|
// return fsck_tree(be, sn.Content)
|
||||||
log.Printf("checking snapshot %s", id)
|
// }
|
||||||
|
|
||||||
sn, err := khepri.LoadSnapshot(repo, id)
|
func commandFsck(be backend.Server, args []string) error {
|
||||||
if err != nil {
|
// var snapshots backend.IDs
|
||||||
return false, err
|
// var err error
|
||||||
}
|
|
||||||
|
|
||||||
return fsck_tree(repo, sn.Content)
|
// if len(args) != 0 {
|
||||||
}
|
// snapshots = make(backend.IDs, 0, len(args))
|
||||||
|
|
||||||
func commandFsck(repo *khepri.Repository, args []string) error {
|
// for _, arg := range args {
|
||||||
var snapshots khepri.IDs
|
// id, err := backend.ParseID(arg)
|
||||||
var err error
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
|
||||||
if len(args) != 0 {
|
// snapshots = append(snapshots, id)
|
||||||
snapshots = make(khepri.IDs, 0, len(args))
|
// }
|
||||||
|
// } else {
|
||||||
|
// snapshots, err = be.ListRefs()
|
||||||
|
|
||||||
for _, arg := range args {
|
// if err != nil {
|
||||||
id, err := khepri.ParseID(arg)
|
// log.Fatalf("error reading list of snapshot IDs: %v", err)
|
||||||
if err != nil {
|
// }
|
||||||
log.Fatal(err)
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
snapshots = append(snapshots, id)
|
// log.Printf("checking %d snapshots", len(snapshots))
|
||||||
}
|
|
||||||
} else {
|
|
||||||
snapshots, err = repo.List(khepri.TYPE_REF)
|
|
||||||
|
|
||||||
if err != nil {
|
// for _, id := range snapshots {
|
||||||
log.Fatalf("error reading list of snapshot IDs: %v", err)
|
// ok, err := fsck_snapshot(be, id)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("checking %d snapshots", len(snapshots))
|
// if err != nil {
|
||||||
|
// log.Printf("error checking snapshot %s: %v", id, err)
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
for _, id := range snapshots {
|
// if !ok {
|
||||||
ok, err := fsck_snapshot(repo, id)
|
// log.Printf("snapshot %s failed", id)
|
||||||
|
// }
|
||||||
if err != nil {
|
// }
|
||||||
log.Printf("error checking snapshot %s: %v", id, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
log.Printf("snapshot %s failed", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,30 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commandInit(path string) error {
|
func commandInit(path string) error {
|
||||||
repo, err := khepri.CreateRepository(path)
|
pw := read_password("enter password for new backend: ")
|
||||||
|
pw2 := read_password("enter password again: ")
|
||||||
|
|
||||||
|
if pw != pw2 {
|
||||||
|
errx(1, "passwords do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
be, err := backend.CreateLocal(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "creating repository at %s failed: %v\n", path, err)
|
fmt.Fprintf(os.Stderr, "creating local backend at %s failed: %v\n", path, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("created khepri repository at %s\n", repo.Path())
|
_, err = khepri.CreateKey(be, pw)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "creating key in local backend at %s failed: %v\n", path, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("created khepri backend at %s\n", be.Location())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,21 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commandList(repo *khepri.Repository, args []string) error {
|
func commandList(be backend.Server, key *khepri.Key, args []string) error {
|
||||||
if len(args) != 1 {
|
|
||||||
return errors.New("usage: list [blob|ref]")
|
|
||||||
}
|
|
||||||
|
|
||||||
tpe := khepri.NewTypeFromString(args[0])
|
// ids, err := be.ListRefs()
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
ids, err := repo.List(tpe)
|
// for _, id := range ids {
|
||||||
if err != nil {
|
// fmt.Printf("%v\n", id)
|
||||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
// }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, id := range ids {
|
|
||||||
fmt.Printf("%v\n", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,34 +2,54 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commandRestore(repo *khepri.Repository, args []string) error {
|
func commandRestore(be backend.Server, key *khepri.Key, args []string) error {
|
||||||
if len(args) != 2 {
|
if len(args) != 2 {
|
||||||
return errors.New("usage: restore ID dir")
|
return errors.New("usage: restore ID dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := khepri.ParseID(args[0])
|
id, err := backend.ParseID(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errx(1, "invalid id %q: %v", args[0], err)
|
errx(1, "invalid id %q: %v", args[0], err)
|
||||||
}
|
}
|
||||||
|
|
||||||
target := args[1]
|
target := args[1]
|
||||||
|
|
||||||
sn, err := khepri.LoadSnapshot(repo, id)
|
// create restorer
|
||||||
|
res, err := khepri.NewRestorer(be, key, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error loading snapshot %s: %v", id, err)
|
fmt.Fprintf(os.Stderr, "creating restorer failed: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = sn.RestoreAt(target)
|
res.Error = func(dir string, node *khepri.Node, err error) error {
|
||||||
if err != nil {
|
fmt.Fprintf(os.Stderr, "error for %s: %+v\n", dir, err)
|
||||||
log.Fatalf("error restoring snapshot %s: %v", id, err)
|
|
||||||
|
// if node.Type == "dir" {
|
||||||
|
// if e, ok := err.(*os.PathError); ok {
|
||||||
|
// if errn, ok := e.Err.(syscall.Errno); ok {
|
||||||
|
// if errn == syscall.EEXIST {
|
||||||
|
// fmt.Printf("ignoring already existing directory %s\n", dir)
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("%q restored to %q\n", id, target)
|
fmt.Printf("restoring %s to %s\n", res.Snapshot(), target)
|
||||||
|
|
||||||
|
err = res.RestoreTo(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,39 +3,34 @@ package main
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TimeFormat = "02.01.2006 15:04:05 -0700"
|
const TimeFormat = "02.01.2006 15:04:05 -0700"
|
||||||
|
|
||||||
func commandSnapshots(repo *khepri.Repository, args []string) error {
|
func commandSnapshots(be backend.Server, key *khepri.Key, args []string) error {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return errors.New("usage: snapshots")
|
return errors.New("usage: snapshots")
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot_ids, err := repo.List(khepri.TYPE_REF)
|
// ch, err := khepri.NewContentHandler(be, key)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Fatalf("error loading list of snapshot ids: %v", err)
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
fmt.Printf("found snapshots:\n")
|
backend.EachID(be, backend.Snapshot, func(id backend.ID) {
|
||||||
for _, id := range snapshot_ids {
|
// sn, err := ch.LoadSnapshot(id)
|
||||||
snapshot, err := khepri.LoadSnapshot(repo, id)
|
// if err != nil {
|
||||||
|
// fmt.Fprintf(os.Stderr, "error loading snapshot %s: %v\n", id, err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
if err != nil {
|
// fmt.Printf("snapshot %s\n %s at %s by %s\n",
|
||||||
log.Printf("error loading snapshot %s: %v", id, err)
|
// id, sn.Dir, sn.Time, sn.Username)
|
||||||
continue
|
fmt.Println(id)
|
||||||
}
|
})
|
||||||
|
|
||||||
fmt.Printf("%s %s@%s %s %s\n",
|
|
||||||
snapshot.Time.Format(TimeFormat),
|
|
||||||
snapshot.Username,
|
|
||||||
snapshot.Hostname,
|
|
||||||
snapshot.Dir,
|
|
||||||
id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.google.com/p/go.crypto/ssh/terminal"
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
"github.com/jessevdk/go-flags"
|
"github.com/jessevdk/go-flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,18 +26,32 @@ func errx(code int, format string, data ...interface{}) {
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
type commandFunc func(*khepri.Repository, []string) error
|
type commandFunc func(backend.Server, *khepri.Key, []string) error
|
||||||
|
|
||||||
var commands map[string]commandFunc
|
var commands map[string]commandFunc
|
||||||
|
|
||||||
|
func read_password(prompt string) string {
|
||||||
|
p := os.Getenv("KHEPRI_PASSWORD")
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(prompt)
|
||||||
|
pw, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
errx(2, "unable to read password: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return string(pw)
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commands = make(map[string]commandFunc)
|
commands = make(map[string]commandFunc)
|
||||||
commands["backup"] = commandBackup
|
commands["backup"] = commandBackup
|
||||||
commands["restore"] = commandRestore
|
commands["restore"] = commandRestore
|
||||||
commands["list"] = commandList
|
commands["list"] = commandList
|
||||||
commands["snapshots"] = commandSnapshots
|
commands["snapshots"] = commandSnapshots
|
||||||
commands["fsck"] = commandFsck
|
|
||||||
commands["dump"] = commandDump
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -42,12 +61,22 @@ func main() {
|
||||||
if Opts.Repo == "" {
|
if Opts.Repo == "" {
|
||||||
Opts.Repo = "khepri-backup"
|
Opts.Repo = "khepri-backup"
|
||||||
}
|
}
|
||||||
args, err := flags.Parse(&Opts)
|
|
||||||
|
|
||||||
|
args, err := flags.Parse(&Opts)
|
||||||
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
|
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
cmds := []string{"init"}
|
||||||
|
for k := range commands {
|
||||||
|
cmds = append(cmds, k)
|
||||||
|
}
|
||||||
|
sort.Strings(cmds)
|
||||||
|
fmt.Printf("nothing to do, available commands: [%v]\n", strings.Join(cmds, "|"))
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
cmd := args[0]
|
cmd := args[0]
|
||||||
|
|
||||||
if cmd == "init" {
|
if cmd == "init" {
|
||||||
|
@ -64,13 +93,18 @@ func main() {
|
||||||
errx(1, "unknown command: %q\n", cmd)
|
errx(1, "unknown command: %q\n", cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := khepri.NewRepository(Opts.Repo)
|
// read_password("enter password: ")
|
||||||
|
repo, err := backend.OpenLocal(Opts.Repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errx(1, "unable to open repo: %v", err)
|
errx(1, "unable to open repo: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f(repo, args[1:])
|
key, err := khepri.SearchKey(repo, read_password("Enter Password for Repository: "))
|
||||||
|
if err != nil {
|
||||||
|
errx(2, "unable to open repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f(repo, key, args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errx(1, "error executing command %q: %v", cmd, err)
|
errx(1, "error executing command %q: %v", cmd, err)
|
||||||
}
|
}
|
||||||
|
|
110
cmd/list/main.go
Normal file
110
cmd/list/main.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"code.google.com/p/go.crypto/ssh/terminal"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
func read_password(prompt string) string {
|
||||||
|
p := os.Getenv("KHEPRI_PASSWORD")
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(prompt)
|
||||||
|
pw, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "unable to read password: %v", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return string(pw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(be backend.Server, key *khepri.Key, t backend.Type) {
|
||||||
|
ids, err := be.List(t)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
buf, err := be.Get(t, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "unable to get snapshot %s: %v\n", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t != backend.Key && t != backend.Blob {
|
||||||
|
buf, err = key.Decrypt(buf)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if t == backend.Snapshot {
|
||||||
|
var sn khepri.Snapshot
|
||||||
|
err = json.Unmarshal(backend.Uncompress(buf), &sn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s %s\n", id, sn.String())
|
||||||
|
} else if t == backend.Blob {
|
||||||
|
fmt.Printf("%s %d bytes (encrypted)\n", id, len(buf))
|
||||||
|
} else if t == backend.Tree {
|
||||||
|
fmt.Printf("%s\n", backend.Hash(buf))
|
||||||
|
} else if t == backend.Key {
|
||||||
|
k := &khepri.Key{}
|
||||||
|
err = json.Unmarshal(buf, k)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "unable to unmashal key: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println(key)
|
||||||
|
} else if t == backend.Lock {
|
||||||
|
fmt.Printf("lock: %v\n", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: archive REPO\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
repo := os.Args[1]
|
||||||
|
|
||||||
|
be, err := backend.OpenLocal(repo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := khepri.SearchKey(be, read_password("Enter Password for Repository: "))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("keys:\n")
|
||||||
|
list(be, key, backend.Key)
|
||||||
|
fmt.Printf("---\nlocks:\n")
|
||||||
|
list(be, key, backend.Lock)
|
||||||
|
fmt.Printf("---\nsnapshots:\n")
|
||||||
|
list(be, key, backend.Snapshot)
|
||||||
|
fmt.Printf("---\ntrees:\n")
|
||||||
|
list(be, key, backend.Tree)
|
||||||
|
fmt.Printf("---\nblobs:\n")
|
||||||
|
list(be, key, backend.Blob)
|
||||||
|
}
|
242
contenthandler.go
Normal file
242
contenthandler.go
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
package khepri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
"github.com/fd0/khepri/chunker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContentHandler struct {
|
||||||
|
be backend.Server
|
||||||
|
key *Key
|
||||||
|
|
||||||
|
content *StorageMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContentHandler creates a new content handler.
|
||||||
|
func NewContentHandler(be backend.Server, key *Key) (*ContentHandler, error) {
|
||||||
|
ch := &ContentHandler{
|
||||||
|
be: be,
|
||||||
|
key: key,
|
||||||
|
content: NewStorageMap(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadSnapshotadds all blobs from a snapshot into the content handler and returns the snapshot.
|
||||||
|
func (ch *ContentHandler) LoadSnapshot(id backend.ID) (*Snapshot, error) {
|
||||||
|
sn, err := LoadSnapshot(ch, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.content.Merge(sn.StorageMap)
|
||||||
|
return sn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAllSnapshots adds all blobs from all snapshots that can be decrypted
|
||||||
|
// into the content handler.
|
||||||
|
func (ch *ContentHandler) LoadAllSnapshots() error {
|
||||||
|
// add all maps from all snapshots that can be decrypted to the storage map
|
||||||
|
err := backend.EachID(ch.be, backend.Snapshot, func(id backend.ID) {
|
||||||
|
sn, err := LoadSnapshot(ch, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch.content.Merge(sn.StorageMap)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save encrypts data and stores it to the backend as type t. If the data was
|
||||||
|
// already saved before, the blob is returned.
|
||||||
|
func (ch *ContentHandler) Save(t backend.Type, data []byte) (*Blob, error) {
|
||||||
|
// compute plaintext hash
|
||||||
|
id := backend.Hash(data)
|
||||||
|
|
||||||
|
// test if the hash is already in the backend
|
||||||
|
blob := ch.content.Find(id)
|
||||||
|
if blob != nil {
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// else create a new blob
|
||||||
|
blob = &Blob{
|
||||||
|
ID: id,
|
||||||
|
Size: uint64(len(data)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt blob
|
||||||
|
ciphertext, err := ch.key.Encrypt(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save blob
|
||||||
|
sid, err := ch.be.Create(t, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blob.Storage = sid
|
||||||
|
blob.StorageSize = uint64(len(ciphertext))
|
||||||
|
|
||||||
|
// insert blob into the storage map
|
||||||
|
ch.content.Insert(blob)
|
||||||
|
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveJSON serialises item as JSON and uses Save() to store it to the backend as type t.
|
||||||
|
func (ch *ContentHandler) SaveJSON(t backend.Type, item interface{}) (*Blob, error) {
|
||||||
|
// convert to json
|
||||||
|
data, err := json.Marshal(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// compress and save data
|
||||||
|
return ch.Save(t, backend.Compress(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFile stores the content of the file on the backend as a Blob by calling
|
||||||
|
// Save for each chunk.
|
||||||
|
func (ch *ContentHandler) SaveFile(filename string, size uint) (Blobs, error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
defer file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the file is small enough, store it directly
|
||||||
|
if size < chunker.MinSize {
|
||||||
|
buf, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err := ch.Save(backend.Blob, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Blobs{blob}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// else store all chunks
|
||||||
|
blobs := Blobs{}
|
||||||
|
chunker := chunker.New(file)
|
||||||
|
|
||||||
|
for {
|
||||||
|
chunk, err := chunker.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err := ch.Save(backend.Blob, chunk.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blobs = append(blobs, blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load tries to load and decrypt content identified by t and id from the backend.
|
||||||
|
func (ch *ContentHandler) Load(t backend.Type, id backend.ID) ([]byte, error) {
|
||||||
|
if t == backend.Snapshot {
|
||||||
|
// load data
|
||||||
|
buf, err := ch.be.Get(t, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt
|
||||||
|
buf, err = ch.key.Decrypt(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup storage hash
|
||||||
|
blob := ch.content.Find(id)
|
||||||
|
if blob == nil {
|
||||||
|
return nil, errors.New("Storage ID not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// load data
|
||||||
|
buf, err := ch.be.Get(t, blob.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check length
|
||||||
|
if len(buf) != int(blob.StorageSize) {
|
||||||
|
return nil, errors.New("Invalid storage length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt
|
||||||
|
buf, err = ch.key.Decrypt(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check length
|
||||||
|
if len(buf) != int(blob.Size) {
|
||||||
|
return nil, errors.New("Invalid length")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadJSON calls Load() to get content from the backend and afterwards calls
|
||||||
|
// json.Unmarshal on the item.
|
||||||
|
func (ch *ContentHandler) LoadJSON(t backend.Type, id backend.ID, item interface{}) error {
|
||||||
|
// load from backend
|
||||||
|
buf, err := ch.Load(t, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// inflate and unmarshal
|
||||||
|
err = json.Unmarshal(backend.Uncompress(buf), item)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadJSONRaw loads data with the given storage id and type from the backend,
|
||||||
|
// decrypts it and calls json.Unmarshal on the item.
|
||||||
|
func (ch *ContentHandler) LoadJSONRaw(t backend.Type, id backend.ID, item interface{}) error {
|
||||||
|
// load data
|
||||||
|
buf, err := ch.be.Get(t, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt
|
||||||
|
buf, err = ch.key.Decrypt(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// inflate and unmarshal
|
||||||
|
err = json.Unmarshal(backend.Uncompress(buf), item)
|
||||||
|
return err
|
||||||
|
}
|
388
key.go
Normal file
388
key.go
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
package khepri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
|
||||||
|
"code.google.com/p/go.crypto/scrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnauthenticated = errors.New("Ciphertext verification failed")
|
||||||
|
ErrNoKeyFound = errors.New("No key could be found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: figure out scrypt values on the fly depending on the current
|
||||||
|
// hardware.
|
||||||
|
const (
|
||||||
|
scrypt_N = 65536
|
||||||
|
scrypt_r = 8
|
||||||
|
scrypt_p = 1
|
||||||
|
scrypt_saltsize = 64
|
||||||
|
aesKeysize = 32 // for AES256
|
||||||
|
hmacKeysize = 32 // for HMAC with SHA256
|
||||||
|
)
|
||||||
|
|
||||||
|
type Key struct {
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
|
||||||
|
KDF string `json:"kdf"`
|
||||||
|
N int `json:"N"`
|
||||||
|
R int `json:"r"`
|
||||||
|
P int `json:"p"`
|
||||||
|
Salt []byte `json:"salt"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
|
||||||
|
user *keys
|
||||||
|
master *keys
|
||||||
|
}
|
||||||
|
|
||||||
|
type keys struct {
|
||||||
|
Sign []byte
|
||||||
|
Encrypt []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateKey(be backend.Server, password string) (*Key, error) {
|
||||||
|
// fill meta data about key
|
||||||
|
k := &Key{
|
||||||
|
Created: time.Now(),
|
||||||
|
KDF: "scrypt",
|
||||||
|
N: scrypt_N,
|
||||||
|
R: scrypt_r,
|
||||||
|
P: scrypt_p,
|
||||||
|
}
|
||||||
|
|
||||||
|
hn, err := os.Hostname()
|
||||||
|
if err == nil {
|
||||||
|
k.Hostname = hn
|
||||||
|
}
|
||||||
|
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err == nil {
|
||||||
|
k.Username = usr.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate random salt
|
||||||
|
k.Salt = make([]byte, scrypt_saltsize)
|
||||||
|
n, err := rand.Read(k.Salt)
|
||||||
|
if n != scrypt_saltsize || err != nil {
|
||||||
|
panic("unable to read enough random bytes for salt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// call scrypt() to derive user key
|
||||||
|
k.user, err = k.scrypt(password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate new random master keys
|
||||||
|
k.master, err = k.newKeys()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt master keys (as json) with user key
|
||||||
|
buf, err := json.Marshal(k.master)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
k.Data, err = k.EncryptUser(buf)
|
||||||
|
|
||||||
|
// dump as json
|
||||||
|
buf, err = json.Marshal(k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// store in repository and return
|
||||||
|
_, err = be.Create(backend.Key, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenKey(be backend.Server, id backend.ID, password string) (*Key, error) {
|
||||||
|
// extract data from repo
|
||||||
|
data, err := be.Get(backend.Key, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore json
|
||||||
|
k := &Key{}
|
||||||
|
err = json.Unmarshal(data, k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check KDF
|
||||||
|
if k.KDF != "scrypt" {
|
||||||
|
return nil, errors.New("only supported KDF is scrypt()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// derive user key
|
||||||
|
k.user, err = k.scrypt(password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt master keys
|
||||||
|
buf, err := k.DecryptUser(k.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore json
|
||||||
|
k.master = &keys{}
|
||||||
|
err = json.Unmarshal(buf, k.master)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchKey(be backend.Server, password string) (*Key, error) {
|
||||||
|
// list all keys
|
||||||
|
ids, err := be.List(backend.Key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// try all keys in repo
|
||||||
|
var key *Key
|
||||||
|
for _, id := range ids {
|
||||||
|
key, err = OpenKey(be, id, password)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNoKeyFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) scrypt(password string) (*keys, error) {
|
||||||
|
if len(k.Salt) == 0 {
|
||||||
|
return nil, fmt.Errorf("scrypt() called with empty salt")
|
||||||
|
}
|
||||||
|
|
||||||
|
keybytes := hmacKeysize + aesKeysize
|
||||||
|
scrypt_keys, err := scrypt.Key([]byte(password), k.Salt, k.N, k.R, k.P, keybytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error deriving keys from password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scrypt_keys) != keybytes {
|
||||||
|
return nil, fmt.Errorf("invalid numbers of bytes expanded from scrypt(): %d", len(scrypt_keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
ks := &keys{
|
||||||
|
Encrypt: scrypt_keys[:aesKeysize],
|
||||||
|
Sign: scrypt_keys[aesKeysize:],
|
||||||
|
}
|
||||||
|
return ks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) newKeys() (*keys, error) {
|
||||||
|
ks := &keys{
|
||||||
|
Encrypt: make([]byte, aesKeysize),
|
||||||
|
Sign: make([]byte, hmacKeysize),
|
||||||
|
}
|
||||||
|
n, err := rand.Read(ks.Encrypt)
|
||||||
|
if n != aesKeysize || err != nil {
|
||||||
|
panic("unable to read enough random bytes for encryption key")
|
||||||
|
}
|
||||||
|
n, err = rand.Read(ks.Sign)
|
||||||
|
if n != hmacKeysize || err != nil {
|
||||||
|
panic("unable to read enough random bytes for signing key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) newIV() ([]byte, error) {
|
||||||
|
buf := make([]byte, aes.BlockSize)
|
||||||
|
_, err := io.ReadFull(rand.Reader, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) pad(plaintext []byte) []byte {
|
||||||
|
l := aes.BlockSize - (len(plaintext) % aes.BlockSize)
|
||||||
|
if l == 0 {
|
||||||
|
l = aes.BlockSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if l <= 0 || l > aes.BlockSize {
|
||||||
|
panic("invalid padding size")
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(plaintext, bytes.Repeat([]byte{byte(l)}, l)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) unpad(plaintext []byte) []byte {
|
||||||
|
l := len(plaintext)
|
||||||
|
pad := plaintext[l-1]
|
||||||
|
|
||||||
|
if pad > aes.BlockSize {
|
||||||
|
panic(errors.New("padding > BlockSize"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if pad == 0 {
|
||||||
|
panic(errors.New("invalid padding 0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := l - int(pad); i < l; i++ {
|
||||||
|
if plaintext[i] != pad {
|
||||||
|
panic(errors.New("invalid padding!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext[:l-int(pad)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts and signs data. Returned is IV || Ciphertext || HMAC. For
|
||||||
|
// the hash function, SHA256 is used, so the overhead is 16+32=48 byte.
|
||||||
|
func (k *Key) encrypt(ks *keys, plaintext []byte) ([]byte, error) {
|
||||||
|
iv, err := k.newIV()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to generate new random iv: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := aes.NewCipher(ks.Encrypt)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to create cipher: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
e := cipher.NewCBCEncrypter(c, iv)
|
||||||
|
p := k.pad(plaintext)
|
||||||
|
ciphertext := make([]byte, len(p))
|
||||||
|
e.CryptBlocks(ciphertext, p)
|
||||||
|
|
||||||
|
ciphertext = append(iv, ciphertext...)
|
||||||
|
|
||||||
|
hm := hmac.New(sha256.New, ks.Sign)
|
||||||
|
|
||||||
|
n, err := hm.Write(ciphertext)
|
||||||
|
if err != nil || n != len(ciphertext) {
|
||||||
|
panic(fmt.Sprintf("unable to calculate hmac of ciphertext: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return hm.Sum(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptUser encrypts and signs data with the user key. Returned is IV ||
|
||||||
|
// Ciphertext || HMAC. For the hash function, SHA256 is used, so the overhead
|
||||||
|
// is 16+32=48 byte.
|
||||||
|
func (k *Key) EncryptUser(plaintext []byte) ([]byte, error) {
|
||||||
|
return k.encrypt(k.user, plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts and signs data with the master key. Returned is IV ||
|
||||||
|
// Ciphertext || HMAC. For the hash function, SHA256 is used, so the overhead
|
||||||
|
// is 16+32=48 byte.
|
||||||
|
func (k *Key) Encrypt(plaintext []byte) ([]byte, error) {
|
||||||
|
return k.encrypt(k.master, plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt verifes and decrypts the ciphertext. Ciphertext must be in the form
|
||||||
|
// IV || Ciphertext || HMAC.
|
||||||
|
func (k *Key) decrypt(ks *keys, ciphertext []byte) ([]byte, error) {
|
||||||
|
hm := hmac.New(sha256.New, ks.Sign)
|
||||||
|
|
||||||
|
// extract hmac
|
||||||
|
l := len(ciphertext) - hm.Size()
|
||||||
|
ciphertext, mac := ciphertext[:l], ciphertext[l:]
|
||||||
|
|
||||||
|
// calculate new hmac
|
||||||
|
n, err := hm.Write(ciphertext)
|
||||||
|
if err != nil || n != len(ciphertext) {
|
||||||
|
panic(fmt.Sprintf("unable to calculate hmac of ciphertext, err %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify hmac
|
||||||
|
mac2 := hm.Sum(nil)
|
||||||
|
|
||||||
|
if !hmac.Equal(mac, mac2) {
|
||||||
|
return nil, ErrUnauthenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract iv
|
||||||
|
iv, ciphertext := ciphertext[:aes.BlockSize], ciphertext[aes.BlockSize:]
|
||||||
|
|
||||||
|
// decrypt data
|
||||||
|
c, err := aes.NewCipher(ks.Encrypt)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to create cipher: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt
|
||||||
|
e := cipher.NewCBCDecrypter(c, iv)
|
||||||
|
plaintext := make([]byte, len(ciphertext))
|
||||||
|
e.CryptBlocks(plaintext, ciphertext)
|
||||||
|
|
||||||
|
// remove padding and return
|
||||||
|
return k.unpad(plaintext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt verifes and decrypts the ciphertext with the master key. Ciphertext
|
||||||
|
// must be in the form IV || Ciphertext || HMAC.
|
||||||
|
func (k *Key) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||||
|
return k.decrypt(k.master, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptUser verifes and decrypts the ciphertext with the master key. Ciphertext
|
||||||
|
// must be in the form IV || Ciphertext || HMAC.
|
||||||
|
func (k *Key) DecryptUser(ciphertext []byte) ([]byte, error) {
|
||||||
|
return k.decrypt(k.user, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each calls backend.Each() with the given parameters, Decrypt() on the
|
||||||
|
// ciphertext and, on successful decryption, f with the plaintext.
|
||||||
|
func (k *Key) Each(be backend.Server, t backend.Type, f func(backend.ID, []byte, error)) error {
|
||||||
|
return backend.Each(be, t, func(id backend.ID, data []byte, e error) {
|
||||||
|
if e != nil {
|
||||||
|
f(id, nil, e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := k.Decrypt(data)
|
||||||
|
if err != nil {
|
||||||
|
f(id, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f(id, buf, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) String() string {
|
||||||
|
if k == nil {
|
||||||
|
return "<Key nil>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<Key of %s@%s, created on %s>", k.Username, k.Hostname, k.Created)
|
||||||
|
}
|
79
key_int_test.go
Normal file
79
key_int_test.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package khepri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var test_values = []struct {
|
||||||
|
ekey, skey []byte
|
||||||
|
ciphertext []byte
|
||||||
|
plaintext []byte
|
||||||
|
should_panic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ekey: decode_hex("303e8687b1d7db18421bdc6bb8588ccadac4d59ee87b8ff70c44e635790cafef"),
|
||||||
|
skey: decode_hex("cc8d4b948ee0ebfe1d415de921d10353ef4d8824cb80b2bcc5fbff8a9b12a42c"),
|
||||||
|
ciphertext: decode_hex("154f582d77e6430409da392c3a09aa38e00a78bcc8919557fe18dd17f83e7b0b3053def59f4215b6e1c6b72ceb5acdddd8511ce3a853e054218de1e9f34637470d68f1f93ba8228e4d9817d7c9acfcd2"),
|
||||||
|
plaintext: []byte("Dies ist ein Test!"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode_hex(s string) []byte {
|
||||||
|
d, _ := hex.DecodeString(s)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if function called panic
|
||||||
|
func should_panic(f func()) (did_panic bool) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
did_panic = true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
f()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCrypto(t *testing.T) {
|
||||||
|
r := &Key{}
|
||||||
|
|
||||||
|
for _, tv := range test_values {
|
||||||
|
// test encryption
|
||||||
|
r.master = &keys{
|
||||||
|
Encrypt: tv.ekey,
|
||||||
|
Sign: tv.skey,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := r.encrypt(r.master, tv.plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt message
|
||||||
|
_, err = r.decrypt(r.master, msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// change hmac, this must fail
|
||||||
|
msg[len(msg)-8] ^= 0x23
|
||||||
|
|
||||||
|
if _, err = r.decrypt(r.master, msg); err != ErrUnauthenticated {
|
||||||
|
t.Fatal("wrong HMAC value not detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// test decryption
|
||||||
|
p, err := r.decrypt(r.master, tv.ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, tv.plaintext) {
|
||||||
|
t.Fatalf("wrong plaintext: expected %q but got %q\n", tv.plaintext, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
key_test.go
Normal file
50
key_test.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package khepri_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
var test_password = "foobar"
|
||||||
|
var testCleanup = flag.Bool("test.cleanup", true, "clean up after running tests (remove local backend directory with all content)")
|
||||||
|
|
||||||
|
func setupBackend(t *testing.T) *backend.Local {
|
||||||
|
tempdir, err := ioutil.TempDir("", "khepri-test-")
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
b, err := backend.CreateLocal(tempdir)
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
t.Logf("created local backend at %s", tempdir)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardownBackend(t *testing.T, b *backend.Local) {
|
||||||
|
if !*testCleanup {
|
||||||
|
t.Logf("leaving local backend at %s\n", b.Location())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(t, os.RemoveAll(b.Location()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupKey(t *testing.T, be backend.Server, password string) *khepri.Key {
|
||||||
|
c, err := khepri.CreateKey(be, password)
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
t.Logf("created Safe at %s", be.Location())
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafe(t *testing.T) {
|
||||||
|
be := setupBackend(t)
|
||||||
|
defer teardownBackend(t, be)
|
||||||
|
_ = setupKey(t, be, test_password)
|
||||||
|
}
|
27
object.go
27
object.go
|
@ -1,27 +0,0 @@
|
||||||
package khepri
|
|
||||||
|
|
||||||
func (repo *Repository) Create(t Type, data []byte) (ID, error) {
|
|
||||||
// TODO: make sure that tempfile is removed upon error
|
|
||||||
|
|
||||||
// create tempfile in repository
|
|
||||||
var err error
|
|
||||||
file, err := repo.tempFile()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// write data to tempfile
|
|
||||||
_, err = file.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// close tempfile, return id
|
|
||||||
id := IDFromData(data)
|
|
||||||
err = repo.renameFile(file, t, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package khepri_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestObjects(t *testing.T) {
|
|
||||||
repo, err := setupRepo()
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err = teardownRepo(repo)
|
|
||||||
ok(t, err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for _, test := range TestStrings {
|
|
||||||
id, err := repo.Create(khepri.TYPE_BLOB, []byte(test.data))
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
id2, err := khepri.ParseID(test.id)
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
equals(t, id2, id)
|
|
||||||
}
|
|
||||||
}
|
|
326
repository.go
326
repository.go
|
@ -1,326 +0,0 @@
|
||||||
package khepri
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dirMode = 0700
|
|
||||||
blobPath = "blobs"
|
|
||||||
refPath = "refs"
|
|
||||||
tempPath = "tmp"
|
|
||||||
configFileName = "config.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrIDDoesNotExist = errors.New("ID does not exist")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Name stands for the alias given to an ID.
|
|
||||||
type Name string
|
|
||||||
|
|
||||||
func (n Name) Encode() string {
|
|
||||||
return url.QueryEscape(string(n))
|
|
||||||
}
|
|
||||||
|
|
||||||
type HashFunc func() hash.Hash
|
|
||||||
|
|
||||||
type Repository struct {
|
|
||||||
path string
|
|
||||||
hash HashFunc
|
|
||||||
config *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Salt string
|
|
||||||
N uint
|
|
||||||
R uint `json:"r"`
|
|
||||||
P uint `json:"p"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: figure out scrypt values on the fly depending on the current
|
|
||||||
// hardware.
|
|
||||||
const (
|
|
||||||
scrypt_N = 65536
|
|
||||||
scrypt_r = 8
|
|
||||||
scrypt_p = 1
|
|
||||||
scrypt_saltsize = 64
|
|
||||||
)
|
|
||||||
|
|
||||||
type Type int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TYPE_BLOB = iota
|
|
||||||
TYPE_REF
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewTypeFromString(s string) Type {
|
|
||||||
switch s {
|
|
||||||
case "blob":
|
|
||||||
return TYPE_BLOB
|
|
||||||
case "ref":
|
|
||||||
return TYPE_REF
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unknown type %q", s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Type) String() string {
|
|
||||||
switch t {
|
|
||||||
case TYPE_BLOB:
|
|
||||||
return "blob"
|
|
||||||
case TYPE_REF:
|
|
||||||
return "ref"
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unknown type %d", t))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRepository opens a dir-baked repository at the given path.
|
|
||||||
func NewRepository(path string) (*Repository, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
d := &Repository{
|
|
||||||
path: path,
|
|
||||||
hash: sha256.New,
|
|
||||||
}
|
|
||||||
|
|
||||||
d.config, err = d.read_config()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) read_config() (*Config, error) {
|
|
||||||
// try to open config file
|
|
||||||
f, err := os.Open(path.Join(r.path, configFileName))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := new(Config)
|
|
||||||
buf, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(buf, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRepository creates all the necessary files and directories for the
|
|
||||||
// Repository.
|
|
||||||
func CreateRepository(p string) (*Repository, error) {
|
|
||||||
dirs := []string{
|
|
||||||
p,
|
|
||||||
path.Join(p, blobPath),
|
|
||||||
path.Join(p, refPath),
|
|
||||||
path.Join(p, tempPath),
|
|
||||||
}
|
|
||||||
|
|
||||||
var configfile = path.Join(p, configFileName)
|
|
||||||
|
|
||||||
// test if repository directories or config file already exist
|
|
||||||
if _, err := os.Stat(configfile); err == nil {
|
|
||||||
return nil, fmt.Errorf("config file %s already exists", configfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range dirs[1:] {
|
|
||||||
if _, err := os.Stat(d); err == nil {
|
|
||||||
return nil, fmt.Errorf("dir %s already exists", d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create initial json configuration
|
|
||||||
cfg := &Config{
|
|
||||||
N: scrypt_N,
|
|
||||||
R: scrypt_r,
|
|
||||||
P: scrypt_p,
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate salt
|
|
||||||
buf := make([]byte, scrypt_saltsize)
|
|
||||||
n, err := rand.Read(buf)
|
|
||||||
if n != scrypt_saltsize || err != nil {
|
|
||||||
panic("unable to read enough random bytes for salt")
|
|
||||||
}
|
|
||||||
cfg.Salt = hex.EncodeToString(buf)
|
|
||||||
|
|
||||||
// create ps for blobs, refs and temp
|
|
||||||
for _, dir := range dirs {
|
|
||||||
err := os.MkdirAll(dir, dirMode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write config file
|
|
||||||
f, err := os.Create(configfile)
|
|
||||||
defer f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := json.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.Write(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// open repository
|
|
||||||
return NewRepository(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHash changes the hash function used for deriving IDs. Default is SHA256.
|
|
||||||
func (r *Repository) SetHash(h HashFunc) {
|
|
||||||
r.hash = h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path returns the directory used for this repository.
|
|
||||||
func (r *Repository) Path() string {
|
|
||||||
return r.path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return temp directory in correct directory for this repository.
|
|
||||||
func (r *Repository) tempFile() (*os.File, error) {
|
|
||||||
return ioutil.TempFile(path.Join(r.path, tempPath), "temp-")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename temp file to final name according to type and ID.
|
|
||||||
func (r *Repository) renameFile(file *os.File, t Type, id ID) error {
|
|
||||||
filename := path.Join(r.dir(t), id.String())
|
|
||||||
return os.Rename(file.Name(), filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct directory for given Type.
|
|
||||||
func (r *Repository) dir(t Type) string {
|
|
||||||
switch t {
|
|
||||||
case TYPE_BLOB:
|
|
||||||
return path.Join(r.path, blobPath)
|
|
||||||
case TYPE_REF:
|
|
||||||
return path.Join(r.path, refPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unknown type %d", t))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct path for given Type and ID.
|
|
||||||
func (r *Repository) filename(t Type, id ID) string {
|
|
||||||
return path.Join(r.dir(t), id.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test returns true if the given ID exists in the repository.
|
|
||||||
func (r *Repository) Test(t Type, id ID) (bool, error) {
|
|
||||||
// try to open file
|
|
||||||
file, err := os.Open(r.filename(t, id))
|
|
||||||
defer func() {
|
|
||||||
file.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a reader for the content stored under the given ID.
|
|
||||||
func (r *Repository) Get(t Type, id ID) (io.ReadCloser, error) {
|
|
||||||
// try to open file
|
|
||||||
file, err := os.Open(r.filename(t, id))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the content stored at ID.
|
|
||||||
func (r *Repository) Remove(t Type, id ID) error {
|
|
||||||
return os.Remove(r.filename(t, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
type IDs []ID
|
|
||||||
|
|
||||||
// Lists all objects of a given type.
|
|
||||||
func (r *Repository) List(t Type) (IDs, error) {
|
|
||||||
// TODO: use os.Open() and d.Readdirnames() instead of Glob()
|
|
||||||
pattern := path.Join(r.dir(t), "*")
|
|
||||||
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ids := make(IDs, 0, len(matches))
|
|
||||||
|
|
||||||
for _, m := range matches {
|
|
||||||
base := filepath.Base(m)
|
|
||||||
|
|
||||||
if base == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
id, err := ParseID(base)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ids IDs) Len() int {
|
|
||||||
return len(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ids IDs) Less(i, j int) bool {
|
|
||||||
if len(ids[i]) < len(ids[j]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, b := range ids[i] {
|
|
||||||
if b == ids[j][k] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if b < ids[j][k] {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ids IDs) Swap(i, j int) {
|
|
||||||
ids[i], ids[j] = ids[j], ids[i]
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
package khepri_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testCleanup = flag.Bool("test.cleanup", true, "clean up after running tests (remove repository directory with all content)")
|
|
||||||
|
|
||||||
var TestStrings = []struct {
|
|
||||||
id string
|
|
||||||
t khepri.Type
|
|
||||||
data string
|
|
||||||
}{
|
|
||||||
{"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", khepri.TYPE_BLOB, "foobar"},
|
|
||||||
{"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", khepri.TYPE_BLOB, "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"},
|
|
||||||
{"cc5d46bdb4991c6eae3eb739c9c8a7a46fe9654fab79c47b4fe48383b5b25e1c", khepri.TYPE_REF, "foo/bar"},
|
|
||||||
{"4e54d2c721cbdb730f01b10b62dec622962b36966ec685880effa63d71c808f2", khepri.TYPE_BLOB, "foo/../../baz"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRepo() (*khepri.Repository, error) {
|
|
||||||
tempdir, err := ioutil.TempDir("", "khepri-test-")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := khepri.CreateRepository(tempdir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func teardownRepo(repo *khepri.Repository) error {
|
|
||||||
if !*testCleanup {
|
|
||||||
fmt.Fprintf(os.Stderr, "leaving repository at %s\n", repo.Path())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.RemoveAll(repo.Path())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepository(t *testing.T) {
|
|
||||||
repo, err := setupRepo()
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err = teardownRepo(repo)
|
|
||||||
ok(t, err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// detect non-existing files
|
|
||||||
for _, test := range TestStrings {
|
|
||||||
id, err := khepri.ParseID(test.id)
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
// try to get string out, should fail
|
|
||||||
ret, err := repo.Test(test.t, id)
|
|
||||||
ok(t, err)
|
|
||||||
assert(t, !ret, fmt.Sprintf("id %q was found (but should not have)", test.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// add files
|
|
||||||
for _, test := range TestStrings {
|
|
||||||
// store string in repository
|
|
||||||
id, err := repo.Create(test.t, []byte(test.data))
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
equals(t, test.id, id.String())
|
|
||||||
|
|
||||||
// try to get it out again
|
|
||||||
rd, err := repo.Get(test.t, id)
|
|
||||||
ok(t, err)
|
|
||||||
assert(t, rd != nil, "Get() returned nil reader")
|
|
||||||
|
|
||||||
// compare content
|
|
||||||
buf, err := ioutil.ReadAll(rd)
|
|
||||||
equals(t, test.data, string(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
// list ids
|
|
||||||
for _, tpe := range []khepri.Type{khepri.TYPE_BLOB, khepri.TYPE_REF} {
|
|
||||||
IDs := khepri.IDs{}
|
|
||||||
for _, test := range TestStrings {
|
|
||||||
if test.t == tpe {
|
|
||||||
id, err := khepri.ParseID(test.id)
|
|
||||||
ok(t, err)
|
|
||||||
IDs = append(IDs, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ids, err := repo.List(tpe)
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
sort.Sort(ids)
|
|
||||||
sort.Sort(IDs)
|
|
||||||
equals(t, IDs, ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove content if requested
|
|
||||||
if *testCleanup {
|
|
||||||
for _, test := range TestStrings {
|
|
||||||
id, err := khepri.ParseID(test.id)
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
found, err := repo.Test(test.t, id)
|
|
||||||
ok(t, err)
|
|
||||||
assert(t, found, fmt.Sprintf("id %q was not found before removal"))
|
|
||||||
|
|
||||||
err = repo.Remove(test.t, id)
|
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
found, err = repo.Test(test.t, id)
|
|
||||||
ok(t, err)
|
|
||||||
assert(t, !found, fmt.Sprintf("id %q was not found before removal"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
100
restorer.go
Normal file
100
restorer.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package khepri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Restorer struct {
|
||||||
|
be backend.Server
|
||||||
|
key *Key
|
||||||
|
ch *ContentHandler
|
||||||
|
sn *Snapshot
|
||||||
|
|
||||||
|
Error func(dir string, node *Node, err error) error
|
||||||
|
Filter func(item string, node *Node) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRestorer creates a restorer preloaded with the content from the snapshot snid.
|
||||||
|
func NewRestorer(be backend.Server, key *Key, snid backend.ID) (*Restorer, error) {
|
||||||
|
r := &Restorer{
|
||||||
|
be: be,
|
||||||
|
key: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
r.ch, err = NewContentHandler(be, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.sn, err = r.ch.LoadSnapshot(snid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// abort on all errors
|
||||||
|
r.Error = func(string, *Node, error) error { return err }
|
||||||
|
// allow all files
|
||||||
|
r.Filter = func(string, *Node) bool { return true }
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Restorer) to(dir string, tree_id backend.ID) error {
|
||||||
|
tree := Tree{}
|
||||||
|
err := res.ch.LoadJSON(backend.Tree, tree_id, &tree)
|
||||||
|
if err != nil {
|
||||||
|
return res.Error(dir, nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range tree {
|
||||||
|
p := filepath.Join(dir, node.Name)
|
||||||
|
if !res.Filter(p, node) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := node.CreateAt(res.ch, p)
|
||||||
|
if err != nil {
|
||||||
|
err = res.Error(p, node, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type == "dir" {
|
||||||
|
if node.Subtree == nil {
|
||||||
|
return errors.New(fmt.Sprintf("Dir without subtree in tree %s", tree_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = res.to(p, node.Subtree)
|
||||||
|
if err != nil {
|
||||||
|
err = res.Error(p, node, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreTo creates the directories and files in the snapshot below dir.
|
||||||
|
// Before an item is created, res.Filter is called.
|
||||||
|
func (res *Restorer) RestoreTo(dir string) error {
|
||||||
|
err := os.MkdirAll(dir, 0700)
|
||||||
|
if err != nil && err != os.ErrExist {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.to(dir, res.sn.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Restorer) Snapshot() *Snapshot {
|
||||||
|
return res.sn
|
||||||
|
}
|
87
snapshot.go
87
snapshot.go
|
@ -1,29 +1,36 @@
|
||||||
package khepri
|
package khepri
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
Content ID `json:"content"`
|
Content backend.ID `json:"content"`
|
||||||
Tree *Tree `json:"-"`
|
StorageMap *StorageMap `json:"map"`
|
||||||
Dir string `json:"dir"`
|
Dir string `json:"dir"`
|
||||||
Hostname string `json:"hostname,omitempty"`
|
Hostname string `json:"hostname,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
UID string `json:"uid,omitempty"`
|
UID string `json:"uid,omitempty"`
|
||||||
GID string `json:"gid,omitempty"`
|
GID string `json:"gid,omitempty"`
|
||||||
id ID `json:omit`
|
|
||||||
repo *Repository
|
id backend.ID // plaintext ID, used during restore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSnapshot(dir string) *Snapshot {
|
func NewSnapshot(dir string) *Snapshot {
|
||||||
|
d, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
d = dir
|
||||||
|
}
|
||||||
|
|
||||||
sn := &Snapshot{
|
sn := &Snapshot{
|
||||||
Dir: dir,
|
Dir: d,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,66 +49,16 @@ func NewSnapshot(dir string) *Snapshot {
|
||||||
return sn
|
return sn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sn *Snapshot) Save(repo *Repository) (ID, error) {
|
func LoadSnapshot(ch *ContentHandler, id backend.ID) (*Snapshot, error) {
|
||||||
if sn.Content == nil {
|
|
||||||
panic("Snapshot.Save() called with nil tree id")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(sn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := repo.Create(TYPE_REF, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadSnapshot(repo *Repository, id ID) (*Snapshot, error) {
|
|
||||||
rd, err := repo.Get(TYPE_REF, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: maybe inject a hashing reader here and test if the given id is correct
|
|
||||||
|
|
||||||
dec := json.NewDecoder(rd)
|
|
||||||
sn := &Snapshot{}
|
sn := &Snapshot{}
|
||||||
err = dec.Decode(sn)
|
err := ch.LoadJSON(backend.Snapshot, id, sn)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sn.id = id
|
|
||||||
sn.repo = repo
|
|
||||||
|
|
||||||
return sn, nil
|
return sn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sn *Snapshot) RestoreAt(path string) error {
|
|
||||||
err := os.MkdirAll(path, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if sn.Tree == nil {
|
|
||||||
sn.Tree, err = NewTreeFromRepo(sn.repo, sn.Content)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sn.Tree.CreateAt(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sn *Snapshot) ID() ID {
|
|
||||||
return sn.id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sn *Snapshot) String() string {
|
func (sn *Snapshot) String() string {
|
||||||
return fmt.Sprintf("<Snapshot of %q at %s>", sn.Dir, sn.Time.Format(time.RFC822Z))
|
return fmt.Sprintf("<Snapshot %q at %s>", sn.Dir, sn.Time)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,23 +5,24 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fd0/khepri"
|
"github.com/fd0/khepri"
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSnapshot(t *testing.T) {
|
func testSnapshot(t *testing.T, be backend.Server) {
|
||||||
repo, err := setupRepo()
|
var err error
|
||||||
ok(t, err)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err = teardownRepo(repo)
|
|
||||||
ok(t, err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
sn := khepri.NewSnapshot("/home/foobar")
|
sn := khepri.NewSnapshot("/home/foobar")
|
||||||
sn.Content, err = khepri.ParseID("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")
|
sn.Content, err = backend.ParseID("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")
|
||||||
ok(t, err)
|
ok(t, err)
|
||||||
sn.Time, err = time.Parse(time.RFC3339Nano, "2014-08-03T17:49:05.378595539+02:00")
|
sn.Time, err = time.Parse(time.RFC3339Nano, "2014-08-03T17:49:05.378595539+02:00")
|
||||||
ok(t, err)
|
ok(t, err)
|
||||||
|
|
||||||
_, err = sn.Save(repo)
|
// _, err = sn.Save(be)
|
||||||
ok(t, err)
|
// ok(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshot(t *testing.T) {
|
||||||
|
repo := setupBackend(t)
|
||||||
|
defer teardownBackend(t, repo)
|
||||||
|
|
||||||
|
testSnapshot(t, repo)
|
||||||
}
|
}
|
||||||
|
|
51
storagemap.go
Normal file
51
storagemap.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package khepri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/fd0/khepri/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageMap Blobs
|
||||||
|
|
||||||
|
func NewStorageMap() *StorageMap {
|
||||||
|
return &StorageMap{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StorageMap) find(id backend.ID) (int, *Blob) {
|
||||||
|
i := sort.Search(len(m), func(i int) bool {
|
||||||
|
return bytes.Compare(m[i].ID, id) >= 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if i < len(m) && bytes.Equal(m[i].ID, id) {
|
||||||
|
return i, m[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StorageMap) Find(id backend.ID) *Blob {
|
||||||
|
_, blob := m.find(id)
|
||||||
|
return blob
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StorageMap) Insert(blob *Blob) {
|
||||||
|
pos, b := m.find(blob.ID)
|
||||||
|
if b != nil {
|
||||||
|
// already present
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert blob
|
||||||
|
// https://code.google.com/p/go-wiki/wiki/SliceTricks
|
||||||
|
*m = append(*m, nil)
|
||||||
|
copy((*m)[pos+1:], (*m)[pos:])
|
||||||
|
(*m)[pos] = blob
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StorageMap) Merge(sm *StorageMap) {
|
||||||
|
for _, blob := range *sm {
|
||||||
|
m.Insert(blob)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ prepare() {
|
||||||
export BASE="$(mktemp --tmpdir --directory khepri-testsuite-XXXXXX)"
|
export BASE="$(mktemp --tmpdir --directory khepri-testsuite-XXXXXX)"
|
||||||
export KHEPRI_REPOSITORY="${BASE}/khepri-backup"
|
export KHEPRI_REPOSITORY="${BASE}/khepri-backup"
|
||||||
export DATADIR="${BASE}/fake-data"
|
export DATADIR="${BASE}/fake-data"
|
||||||
|
export KHEPRI_PASSWORD="foobar"
|
||||||
debug "repository is at ${KHEPRI_REPOSITORY}"
|
debug "repository is at ${KHEPRI_REPOSITORY}"
|
||||||
|
|
||||||
mkdir -p "$DATADIR"
|
mkdir -p "$DATADIR"
|
||||||
|
|
|
@ -3,6 +3,6 @@ set -e
|
||||||
prepare
|
prepare
|
||||||
run khepri init
|
run khepri init
|
||||||
run khepri backup "${BASE}/fake-data"
|
run khepri backup "${BASE}/fake-data"
|
||||||
run khepri restore "$(khepri list ref)" "${BASE}/fake-data-restore"
|
run khepri restore "$(khepri snapshots)" "${BASE}/fake-data-restore"
|
||||||
dirdiff "${BASE}/fake-data" "${BASE}/fake-data-restore"
|
dirdiff "${BASE}/fake-data" "${BASE}/fake-data-restore/fake-data"
|
||||||
cleanup
|
cleanup
|
||||||
|
|
307
tree.go
307
tree.go
|
@ -1,248 +1,71 @@
|
||||||
package khepri
|
package khepri
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fd0/khepri/chunker"
|
"github.com/fd0/khepri/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tree struct {
|
type Tree []*Node
|
||||||
Nodes []*Node `json:"nodes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Mode os.FileMode `json:"mode,omitempty"`
|
Mode os.FileMode `json:"mode,omitempty"`
|
||||||
ModTime time.Time `json:"mtime,omitempty"`
|
ModTime time.Time `json:"mtime,omitempty"`
|
||||||
AccessTime time.Time `json:"atime,omitempty"`
|
AccessTime time.Time `json:"atime,omitempty"`
|
||||||
ChangeTime time.Time `json:"ctime,omitempty"`
|
ChangeTime time.Time `json:"ctime,omitempty"`
|
||||||
UID uint32 `json:"uid"`
|
UID uint32 `json:"uid"`
|
||||||
GID uint32 `json:"gid"`
|
GID uint32 `json:"gid"`
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
Inode uint64 `json:"inode,omitempty"`
|
Inode uint64 `json:"inode,omitempty"`
|
||||||
Size uint64 `json:"size,omitempty"`
|
Size uint64 `json:"size,omitempty"`
|
||||||
Links uint64 `json:"links,omitempty"`
|
Links uint64 `json:"links,omitempty"`
|
||||||
LinkTarget string `json:"linktarget,omitempty"`
|
LinkTarget string `json:"linktarget,omitempty"`
|
||||||
Device uint64 `json:"device,omitempty"`
|
Device uint64 `json:"device,omitempty"`
|
||||||
Content []ID `json:"content,omitempty"`
|
Content []backend.ID `json:"content,omitempty"`
|
||||||
Subtree ID `json:"subtree,omitempty"`
|
Subtree backend.ID `json:"subtree,omitempty"`
|
||||||
Tree *Tree `json:"-"`
|
|
||||||
repo *Repository
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTree() *Tree {
|
type Blob struct {
|
||||||
return &Tree{
|
ID backend.ID `json:"id,omitempty"`
|
||||||
Nodes: []*Node{},
|
Size uint64 `json:"size,omitempty"`
|
||||||
}
|
Storage backend.ID `json:"sid,omitempty"` // encrypted ID
|
||||||
|
StorageSize uint64 `json:"ssize,omitempty"` // encrypted Size
|
||||||
}
|
}
|
||||||
|
|
||||||
func store_chunk(repo *Repository, rd io.Reader) (ID, error) {
|
type Blobs []*Blob
|
||||||
data, err := ioutil.ReadAll(rd)
|
|
||||||
if err != nil {
|
func (n Node) String() string {
|
||||||
return nil, err
|
switch n.Type {
|
||||||
|
case "file":
|
||||||
|
return fmt.Sprintf("%s %5d %5d %6d %s %s",
|
||||||
|
n.Mode, n.UID, n.GID, n.Size, n.ModTime, n.Name)
|
||||||
|
case "dir":
|
||||||
|
return fmt.Sprintf("%s %5d %5d %6d %s %s",
|
||||||
|
n.Mode|os.ModeDir, n.UID, n.GID, n.Size, n.ModTime, n.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := repo.Create(TYPE_BLOB, data)
|
return fmt.Sprintf("<Node(%s) %s>", n.Type, n.Name)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTreeFromPath(repo *Repository, dir string) (*Tree, error) {
|
func (t Tree) String() string {
|
||||||
fd, err := os.Open(dir)
|
s := []string{}
|
||||||
defer fd.Close()
|
for _, n := range t {
|
||||||
if err != nil {
|
s = append(s, n.String())
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
return strings.Join(s, "\n")
|
||||||
entries, err := fd.Readdir(-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree := &Tree{
|
|
||||||
Nodes: make([]*Node, 0, len(entries)),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
path := filepath.Join(dir, entry.Name())
|
|
||||||
node, err := NodeFromFileInfo(path, entry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
node.repo = repo
|
|
||||||
|
|
||||||
tree.Nodes = append(tree.Nodes, node)
|
|
||||||
|
|
||||||
if entry.IsDir() {
|
|
||||||
node.Tree, err = NewTreeFromPath(repo, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Type == "file" {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
defer file.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Size < chunker.MinSize {
|
|
||||||
// if the file is small enough, store it directly
|
|
||||||
id, err := store_chunk(repo, file)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Content = []ID{id}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// else store chunks
|
|
||||||
node.Content = []ID{}
|
|
||||||
ch := chunker.New(file)
|
|
||||||
|
|
||||||
for {
|
|
||||||
chunk, err := ch.Next()
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := store_chunk(repo, bytes.NewBuffer(chunk.Data))
|
|
||||||
|
|
||||||
node.Content = append(node.Content, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tree, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree *Tree) Save(repo *Repository) (ID, error) {
|
|
||||||
for _, node := range tree.Nodes {
|
|
||||||
if node.Tree != nil {
|
|
||||||
var err error
|
|
||||||
node.Subtree, err = node.Tree.Save(repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(tree)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := repo.Create(TYPE_BLOB, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTreeFromRepo(repo *Repository, id ID) (*Tree, error) {
|
|
||||||
tree := NewTree()
|
|
||||||
|
|
||||||
rd, err := repo.Get(TYPE_BLOB, id)
|
|
||||||
defer rd.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(rd)
|
|
||||||
|
|
||||||
err = decoder.Decode(tree)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
|
||||||
node.repo = repo
|
|
||||||
|
|
||||||
if node.Subtree != nil {
|
|
||||||
node.Tree, err = NewTreeFromRepo(repo, node.Subtree)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *Tree) CreateAt(path string) error {
|
|
||||||
for _, node := range tree.Nodes {
|
|
||||||
nodepath := filepath.Join(path, node.Name)
|
|
||||||
|
|
||||||
if node.Type == "dir" {
|
|
||||||
err := os.Mkdir(nodepath, 0700)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Chmod(nodepath, node.Mode)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Chown(nodepath, int(node.UID), int(node.GID))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = node.Tree.CreateAt(filepath.Join(path, node.Name))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Chtimes(nodepath, node.AccessTime, node.ModTime)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
err := node.CreateAt(nodepath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make sure that node.Type is valid
|
|
||||||
|
|
||||||
func (node *Node) fill_extra(path string, fi os.FileInfo) (err error) {
|
func (node *Node) fill_extra(path string, fi os.FileInfo) (err error) {
|
||||||
stat, ok := fi.Sys().(*syscall.Stat_t)
|
stat, ok := fi.Sys().(*syscall.Stat_t)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -290,6 +113,7 @@ func (node *Node) fill_extra(path string, fi os.FileInfo) (err error) {
|
||||||
|
|
||||||
func NodeFromFileInfo(path string, fi os.FileInfo) (*Node, error) {
|
func NodeFromFileInfo(path string, fi os.FileInfo) (*Node, error) {
|
||||||
node := &Node{
|
node := &Node{
|
||||||
|
path: path,
|
||||||
Name: fi.Name(),
|
Name: fi.Name(),
|
||||||
Mode: fi.Mode() & os.ModePerm,
|
Mode: fi.Mode() & os.ModePerm,
|
||||||
ModTime: fi.ModTime(),
|
ModTime: fi.ModTime(),
|
||||||
|
@ -316,12 +140,27 @@ func NodeFromFileInfo(path string, fi os.FileInfo) (*Node, error) {
|
||||||
return node, err
|
return node, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *Node) CreateAt(path string) error {
|
func (node *Node) CreateAt(ch *ContentHandler, path string) error {
|
||||||
if node.repo == nil {
|
|
||||||
return fmt.Errorf("repository is nil!")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch node.Type {
|
switch node.Type {
|
||||||
|
case "dir":
|
||||||
|
err := os.Mkdir(path, node.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Lchown(path, int(node.UID), int(node.GID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var utimes = []syscall.Timespec{
|
||||||
|
syscall.NsecToTimespec(node.AccessTime.UnixNano()),
|
||||||
|
syscall.NsecToTimespec(node.ModTime.UnixNano()),
|
||||||
|
}
|
||||||
|
err = syscall.UtimesNano(path, utimes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case "file":
|
case "file":
|
||||||
// TODO: handle hard links
|
// TODO: handle hard links
|
||||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
@ -331,18 +170,32 @@ func (node *Node) CreateAt(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, blobid := range node.Content {
|
for _, blobid := range node.Content {
|
||||||
rd, err := node.repo.Get(TYPE_BLOB, blobid)
|
buf, err := ch.Load(backend.Blob, blobid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(f, rd)
|
_, err = f.Write(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
|
err = os.Lchown(path, int(node.UID), int(node.GID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var utimes = []syscall.Timespec{
|
||||||
|
syscall.NsecToTimespec(node.AccessTime.UnixNano()),
|
||||||
|
syscall.NsecToTimespec(node.ModTime.UnixNano()),
|
||||||
|
}
|
||||||
|
err = syscall.UtimesNano(path, utimes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case "symlink":
|
case "symlink":
|
||||||
err := os.Symlink(node.LinkTarget, path)
|
err := os.Symlink(node.LinkTarget, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
54
tree_test.go
Normal file
54
tree_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package khepri_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testFiles = []struct {
|
||||||
|
name string
|
||||||
|
content []byte
|
||||||
|
}{
|
||||||
|
{"foo", []byte("bar")},
|
||||||
|
{"bar/foo2", []byte("bar2")},
|
||||||
|
{"bar/bla/blubb", []byte("This is just a test!\n")},
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare directory and return temporary path
|
||||||
|
func prepare_dir(t *testing.T) string {
|
||||||
|
tempdir, err := ioutil.TempDir("", "khepri-test-")
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
for _, test := range testFiles {
|
||||||
|
file := filepath.Join(tempdir, test.name)
|
||||||
|
dir := filepath.Dir(file)
|
||||||
|
if dir != "." {
|
||||||
|
ok(t, os.MkdirAll(dir, 0755))
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(file)
|
||||||
|
defer func() {
|
||||||
|
ok(t, f.Close())
|
||||||
|
}()
|
||||||
|
|
||||||
|
ok(t, err)
|
||||||
|
|
||||||
|
_, err = f.Write(test.content)
|
||||||
|
ok(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("tempdir prepared at %s", tempdir)
|
||||||
|
|
||||||
|
return tempdir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTree(t *testing.T) {
|
||||||
|
dir := prepare_dir(t)
|
||||||
|
defer func() {
|
||||||
|
if *testCleanup {
|
||||||
|
ok(t, os.RemoveAll(dir))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
Loading…
Reference in a new issue