forked from TrueCloudLab/restic
234 lines
4 KiB
Go
234 lines
4 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 HashFunc func() hash.Hash
|
|
|
|
type Repository struct {
|
|
path string
|
|
hash HashFunc
|
|
}
|
|
|
|
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 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]
|
|
}
|