Refactor, add Object and Snapshot

This commit is contained in:
Alexander Neumann 2014-08-04 20:47:04 +02:00
parent fbd33636f0
commit b3c2d82331
10 changed files with 281 additions and 29 deletions

View file

@ -24,8 +24,9 @@ func hash(filename string) (khepri.ID, error) {
return h.Sum([]byte{}), nil
}
func archive_dir(repo *khepri.DirRepository, path string) (khepri.ID, error) {
func archive_dir(repo *khepri.Repository, path string) (khepri.ID, error) {
log.Printf("archiving dir %q", path)
dir, err := os.Open(path)
if err != nil {
log.Printf("open(%q): %v\n", path, err)
@ -81,7 +82,7 @@ func archive_dir(repo *khepri.DirRepository, path string) (khepri.ID, error) {
return id, nil
}
func commandBackup(repo *khepri.DirRepository, args []string) error {
func commandBackup(repo *khepri.Repository, args []string) error {
if len(args) != 1 {
return errors.New("usage: backup dir")
}
@ -93,7 +94,11 @@ func commandBackup(repo *khepri.DirRepository, args []string) error {
return err
}
fmt.Printf("%q archived as %v\n", target, id)
sn := repo.NewSnapshot(target)
sn.Tree = id
sn.Save()
fmt.Printf("%q archived as %v\n", target, sn.ID())
return nil
}

View file

@ -8,7 +8,7 @@ import (
"github.com/fd0/khepri"
)
func commandList(repo *khepri.DirRepository, args []string) error {
func commandList(repo *khepri.Repository, args []string) error {
if len(args) != 1 {
return errors.New("usage: list [blob|ref]")
}

View file

@ -10,7 +10,7 @@ import (
"github.com/fd0/khepri"
)
func restore_file(repo *khepri.DirRepository, node khepri.Node, target string) error {
func restore_file(repo *khepri.Repository, node khepri.Node, target string) error {
fmt.Printf(" restore file %q\n", target)
rd, err := repo.Get(khepri.TYPE_BLOB, node.Content)
@ -47,7 +47,7 @@ func restore_file(repo *khepri.DirRepository, node khepri.Node, target string) e
return nil
}
func restore_dir(repo *khepri.DirRepository, id khepri.ID, target string) error {
func restore_dir(repo *khepri.Repository, id khepri.ID, target string) error {
fmt.Printf(" restore dir %q\n", target)
rd, err := repo.Get(khepri.TYPE_REF, id)
if err != nil {
@ -104,7 +104,7 @@ func restore_dir(repo *khepri.DirRepository, id khepri.ID, target string) error
return nil
}
func commandRestore(repo *khepri.DirRepository, args []string) error {
func commandRestore(repo *khepri.Repository, args []string) error {
if len(args) != 2 {
return errors.New("usage: restore ID dir")
}

View file

@ -22,7 +22,7 @@ func errx(code int, format string, data ...interface{}) {
os.Exit(code)
}
type commandFunc func(*khepri.DirRepository, []string) error
type commandFunc func(*khepri.Repository, []string) error
var commands map[string]commandFunc
@ -61,7 +61,7 @@ func main() {
errx(1, "unknown command: %q\n", cmd)
}
repo, err := khepri.NewDirRepository(Opts.Repo)
repo, err := khepri.NewRepository(Opts.Repo)
if err != nil {
errx(1, "unable to create/open repo: %v", err)

119
object.go Normal file
View file

@ -0,0 +1,119 @@
package khepri
import "os"
type Object struct {
repo *Repository
id ID
tpe Type
hw HashingWriter
file *os.File
}
func (repo *Repository) NewObject(t Type) (*Object, error) {
obj := &Object{
repo: repo,
tpe: t,
}
return obj, obj.open()
}
func (obj *Object) open() error {
if obj.isFinal() {
panic("object is finalized")
}
if obj.isOpen() {
panic("object already open")
}
// create tempfile in repository
if obj.hw == nil {
// save contents to tempfile, hash while writing
var err error
obj.file, err = obj.repo.tempFile()
if err != nil {
return err
}
// create hashing writer
obj.hw = NewHashingWriter(obj.file, obj.repo.hash)
}
return nil
}
func (obj *Object) isOpen() bool {
return obj.file != nil && obj.hw != nil
}
func (obj *Object) isFinal() bool {
return obj.id != nil
}
func (obj *Object) Write(data []byte) (int, error) {
if !obj.isOpen() {
panic("object not open")
}
return obj.hw.Write(data)
}
func (obj *Object) Close() error {
if obj.file == nil || obj.hw == nil {
panic("object is not open")
}
obj.file.Close()
hash := obj.hw.Hash()
// move file to final name using hash of contents
id := ID(hash)
err := obj.repo.renameFile(obj.file, obj.tpe, id)
if err != nil {
return err
}
obj.hw = nil
obj.file = nil
obj.id = id
return nil
}
func (obj *Object) ID() ID {
if !obj.isFinal() {
panic("object not finalized")
}
return obj.id
}
func (obj *Object) Type() Type {
return obj.tpe
}
func (obj *Object) Remove() error {
if obj.id != nil {
return obj.repo.Remove(obj.tpe, obj.id)
}
if obj.file != nil {
file := obj.file
obj.hw = nil
obj.file = nil
err := file.Close()
if err != nil {
return err
}
return os.Remove(file.Name())
}
return nil
}

31
object_test.go Normal file
View file

@ -0,0 +1,31 @@
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 {
obj, err := repo.NewObject(khepri.TYPE_BLOB)
ok(t, err)
_, err = obj.Write([]byte(test.data))
ok(t, err)
obj.Close()
id, err := khepri.ParseID(test.id)
ok(t, err)
equals(t, id, obj.ID())
}
}

View file

@ -31,7 +31,7 @@ func (n Name) Encode() string {
return url.QueryEscape(string(n))
}
type DirRepository struct {
type Repository struct {
path string
hash func() hash.Hash
}
@ -66,8 +66,8 @@ func (t Type) String() string {
}
// NewDirRepository creates a new dir-baked repository at the given path.
func NewDirRepository(path string) (*DirRepository, error) {
d := &DirRepository{
func NewRepository(path string) (*Repository, error) {
d := &Repository{
path: path,
hash: sha256.New,
}
@ -81,7 +81,7 @@ func NewDirRepository(path string) (*DirRepository, error) {
return d, nil
}
func (r *DirRepository) create() error {
func (r *Repository) create() error {
dirs := []string{
r.path,
path.Join(r.path, blobPath),
@ -100,28 +100,28 @@ func (r *DirRepository) create() error {
}
// SetHash changes the hash function used for deriving IDs. Default is SHA256.
func (r *DirRepository) SetHash(h func() hash.Hash) {
func (r *Repository) SetHash(h func() hash.Hash) {
r.hash = h
}
// Path returns the directory used for this repository.
func (r *DirRepository) Path() string {
func (r *Repository) Path() string {
return r.path
}
// Return temp directory in correct directory for this repository.
func (r *DirRepository) tempFile() (*os.File, error) {
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 *DirRepository) renameFile(file *os.File, t Type, id ID) error {
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)
}
// Put saves content and returns the ID.
func (r *DirRepository) Put(t Type, reader io.Reader) (ID, error) {
func (r *Repository) Put(t Type, reader io.Reader) (ID, error) {
// save contents to tempfile, hash while writing
file, err := r.tempFile()
if err != nil {
@ -150,7 +150,7 @@ func (r *DirRepository) Put(t Type, reader io.Reader) (ID, error) {
}
// Construct directory for given Type.
func (r *DirRepository) dir(t Type) string {
func (r *Repository) dir(t Type) string {
switch t {
case TYPE_BLOB:
return path.Join(r.path, blobPath)
@ -162,12 +162,12 @@ func (r *DirRepository) dir(t Type) string {
}
// Construct path for given Type and ID.
func (r *DirRepository) filename(t Type, id ID) string {
func (r *Repository) filename(t Type, id ID) string {
return path.Join(r.dir(t), id.String())
}
// PutFile saves a file's content to the repository and returns the ID.
func (r *DirRepository) PutFile(path string) (ID, error) {
func (r *Repository) PutFile(path string) (ID, error) {
f, err := os.Open(path)
defer f.Close()
if err != nil {
@ -178,7 +178,7 @@ func (r *DirRepository) PutFile(path string) (ID, error) {
}
// PutRaw saves a []byte's content to the repository and returns the ID.
func (r *DirRepository) PutRaw(t Type, buf []byte) (ID, error) {
func (r *Repository) PutRaw(t Type, buf []byte) (ID, error) {
// save contents to tempfile, hash while writing
file, err := r.tempFile()
if err != nil {
@ -206,7 +206,7 @@ func (r *DirRepository) PutRaw(t Type, buf []byte) (ID, error) {
}
// Test returns true if the given ID exists in the repository.
func (r *DirRepository) Test(t Type, id ID) (bool, error) {
func (r *Repository) Test(t Type, id ID) (bool, error) {
// try to open file
file, err := os.Open(r.filename(t, id))
defer func() {
@ -224,7 +224,7 @@ func (r *DirRepository) Test(t Type, id ID) (bool, error) {
}
// Get returns a reader for the content stored under the given ID.
func (r *DirRepository) Get(t Type, id ID) (io.Reader, error) {
func (r *Repository) Get(t Type, id ID) (io.Reader, error) {
// try to open file
file, err := os.Open(r.filename(t, id))
if err != nil {
@ -235,14 +235,14 @@ func (r *DirRepository) Get(t Type, id ID) (io.Reader, error) {
}
// Remove removes the content stored at ID.
func (r *DirRepository) Remove(t Type, id ID) error {
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 *DirRepository) ListIDs(t Type) (IDs, error) {
func (r *Repository) ListIDs(t Type) (IDs, error) {
// TODO: use os.Open() and d.Readdirnames() instead of Glob()
pattern := path.Join(r.dir(t), "*")

View file

@ -25,13 +25,13 @@ var TestStrings = []struct {
{"4e54d2c721cbdb730f01b10b62dec622962b36966ec685880effa63d71c808f2", khepri.TYPE_BLOB, "foo/../../baz"},
}
func setupRepo() (*khepri.DirRepository, error) {
func setupRepo() (*khepri.Repository, error) {
tempdir, err := ioutil.TempDir("", "khepri-test-")
if err != nil {
return nil, err
}
repo, err := khepri.NewDirRepository(tempdir)
repo, err := khepri.NewRepository(tempdir)
if err != nil {
return nil, err
}
@ -39,7 +39,7 @@ func setupRepo() (*khepri.DirRepository, error) {
return repo, nil
}
func teardownRepo(repo *khepri.DirRepository) error {
func teardownRepo(repo *khepri.Repository) error {
if !*testCleanup {
fmt.Fprintf(os.Stderr, "leaving repository at %s\n", repo.Path())
return nil

71
snapshot.go Normal file
View file

@ -0,0 +1,71 @@
package khepri
import (
"encoding/json"
"os"
"os/user"
"time"
)
type Snapshot struct {
Time time.Time `json:"time"`
Tree ID `json:"tree"`
Dir string `json:"dir"`
Hostname string `json:"hostname,omitempty"`
Username string `json:"username,omitempty"`
UID string `json:"uid,omitempty"`
GID string `json:"gid,omitempty"`
id ID
repo *Repository
}
func (repo *Repository) NewSnapshot(dir string) *Snapshot {
sn := &Snapshot{
Dir: dir,
repo: repo,
Time: time.Now(),
}
hn, err := os.Hostname()
if err == nil {
sn.Hostname = hn
}
usr, err := user.Current()
if err == nil {
sn.Username = usr.Username
sn.UID = usr.Uid
sn.GID = usr.Gid
}
return sn
}
func (sn *Snapshot) Save() error {
if sn.Tree == nil {
panic("Snapshot.Save() called with nil tree id")
}
obj, err := sn.repo.NewObject(TYPE_REF)
if err != nil {
return err
}
enc := json.NewEncoder(obj)
err = enc.Encode(sn)
if err != nil {
return err
}
err = obj.Close()
if err != nil {
return err
}
sn.id = obj.ID()
return nil
}
func (sn *Snapshot) ID() ID {
return sn.id
}

26
snapshot_test.go Normal file
View file

@ -0,0 +1,26 @@
package khepri_test
import (
"testing"
"time"
"github.com/fd0/khepri"
)
func TestSnapshot(t *testing.T) {
repo, err := setupRepo()
ok(t, err)
defer func() {
err = teardownRepo(repo)
ok(t, err)
}()
sn := repo.NewSnapshot("/home/foobar")
sn.Tree, err = khepri.ParseID("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")
ok(t, err)
sn.Time, err = time.Parse(time.RFC3339Nano, "2014-08-03T17:49:05.378595539+02:00")
ok(t, err)
ok(t, sn.Save())
}