restic/repository.go

301 lines
5.3 KiB
Go

package khepri
import (
"crypto/sha256"
"errors"
"fmt"
"hash"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
)
const (
dirMode = 0700
blobPath = "blobs"
refPath = "refs"
tempPath = "tmp"
)
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 Repository struct {
path string
hash func() hash.Hash
}
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))
}
// NewDirRepository creates a new dir-baked repository at the given path.
func NewRepository(path string) (*Repository, error) {
d := &Repository{
path: path,
hash: sha256.New,
}
err := d.create()
if err != nil {
return nil, err
}
return d, nil
}
func (r *Repository) create() error {
dirs := []string{
r.path,
path.Join(r.path, blobPath),
path.Join(r.path, refPath),
path.Join(r.path, tempPath),
}
for _, dir := range dirs {
err := os.MkdirAll(dir, dirMode)
if err != nil {
return err
}
}
return nil
}
// SetHash changes the hash function used for deriving IDs. Default is SHA256.
func (r *Repository) SetHash(h func() hash.Hash) {
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)
}
// Put saves content and returns the ID.
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 {
return nil, err
}
rd := NewHashingReader(reader, r.hash)
_, err = io.Copy(file, rd)
if err != nil {
return nil, err
}
err = file.Close()
if err != nil {
return nil, err
}
// move file to final name using hash of contents
id := ID(rd.Hash())
err = r.renameFile(file, t, id)
if err != nil {
return nil, err
}
return id, nil
}
// 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())
}
// PutFile saves a file's content to the repository and returns the ID.
func (r *Repository) PutFile(path string) (ID, error) {
f, err := os.Open(path)
defer f.Close()
if err != nil {
return nil, err
}
return r.Put(TYPE_BLOB, f)
}
// PutRaw saves a []byte's content to the repository and returns the ID.
func (r *Repository) PutRaw(t Type, buf []byte) (ID, error) {
// save contents to tempfile, hash while writing
file, err := r.tempFile()
if err != nil {
return nil, err
}
wr := NewHashingWriter(file, r.hash)
_, err = wr.Write(buf)
if err != nil {
return nil, err
}
err = file.Close()
if err != nil {
return nil, err
}
// move file to final name using hash of contents
id := ID(wr.Hash())
err = r.renameFile(file, t, id)
if err != nil {
return nil, err
}
return id, nil
}
// 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.Reader, 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) ListIDs(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]
}