restic/repository.go

327 lines
5.8 KiB
Go
Raw Normal View History

2014-07-28 18:20:32 +00:00
package khepri
2014-04-19 17:37:34 +00:00
import (
"crypto/rand"
2014-04-19 17:37:34 +00:00
"crypto/sha256"
"encoding/hex"
"encoding/json"
2014-04-19 17:37:34 +00:00
"errors"
"fmt"
2014-04-19 17:37:34 +00:00
"hash"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
2014-04-19 17:37:34 +00:00
)
const (
dirMode = 0700
blobPath = "blobs"
refPath = "refs"
tempPath = "tmp"
configFileName = "config.json"
2014-04-19 17:37:34 +00:00
)
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))
}
2014-08-11 20:47:24 +00:00
type HashFunc func() hash.Hash
2014-08-04 18:47:04 +00:00
type Repository struct {
path string
hash HashFunc
config *Config
2014-04-19 17:37:34 +00:00
}
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 (
2014-08-03 13:16:56 +00:00
TYPE_BLOB = iota
TYPE_REF
)
func NewTypeFromString(s string) Type {
switch s {
case "blob":
2014-08-03 13:16:56 +00:00
return TYPE_BLOB
case "ref":
2014-08-03 13:16:56 +00:00
return TYPE_REF
}
panic(fmt.Sprintf("unknown type %q", s))
}
func (t Type) String() string {
switch t {
2014-08-03 13:16:56 +00:00
case TYPE_BLOB:
return "blob"
2014-08-03 13:16:56 +00:00
case TYPE_REF:
return "ref"
}
panic(fmt.Sprintf("unknown type %d", t))
}
// NewRepository opens a dir-baked repository at the given path.
2014-08-04 18:47:04 +00:00
func NewRepository(path string) (*Repository, error) {
var err error
2014-08-04 18:47:04 +00:00
d := &Repository{
2014-04-19 17:37:34 +00:00
path: path,
hash: sha256.New,
}
d.config, err = d.read_config()
2014-04-19 17:37:34 +00:00
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) {
2014-04-19 17:37:34 +00:00
dirs := []string{
p,
path.Join(p, blobPath),
path.Join(p, refPath),
path.Join(p, tempPath),
2014-04-19 17:37:34 +00:00
}
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
2014-04-19 17:37:34 +00:00
for _, dir := range dirs {
err := os.MkdirAll(dir, dirMode)
if err != nil {
return nil, err
2014-04-19 17:37:34 +00:00
}
}
// 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)
2014-04-19 17:37:34 +00:00
}
2014-08-11 20:47:24 +00:00
// SetHash changes the hash function used for deriving IDs. Default is SHA256.
func (r *Repository) SetHash(h HashFunc) {
r.hash = h
}
2014-04-19 17:37:34 +00:00
// Path returns the directory used for this repository.
2014-08-04 18:47:04 +00:00
func (r *Repository) Path() string {
2014-04-19 17:37:34 +00:00
return r.path
}
// Return temp directory in correct directory for this repository.
2014-08-04 18:47:04 +00:00
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.
2014-08-04 18:47:04 +00:00
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.
2014-08-04 18:47:04 +00:00
func (r *Repository) dir(t Type) string {
switch t {
2014-08-03 13:16:56 +00:00
case TYPE_BLOB:
return path.Join(r.path, blobPath)
2014-08-03 13:16:56 +00:00
case TYPE_REF:
return path.Join(r.path, refPath)
}
panic(fmt.Sprintf("unknown type %d", t))
}
// Construct path for given Type and ID.
2014-08-04 18:47:04 +00:00
func (r *Repository) filename(t Type, id ID) string {
return path.Join(r.dir(t), id.String())
}
2014-04-19 17:37:34 +00:00
// Test returns true if the given ID exists in the repository.
2014-08-04 18:47:04 +00:00
func (r *Repository) Test(t Type, id ID) (bool, error) {
2014-04-19 17:37:34 +00:00
// try to open file
file, err := os.Open(r.filename(t, id))
2014-04-19 17:37:34 +00:00
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.
2014-08-04 20:46:14 +00:00
func (r *Repository) Get(t Type, id ID) (io.ReadCloser, error) {
2014-04-19 17:37:34 +00:00
// try to open file
file, err := os.Open(r.filename(t, id))
2014-04-19 17:37:34 +00:00
if err != nil {
return nil, err
}
return file, nil
}
2014-04-19 17:46:20 +00:00
// Remove removes the content stored at ID.
2014-08-04 18:47:04 +00:00
func (r *Repository) Remove(t Type, id ID) error {
return os.Remove(r.filename(t, id))
2014-04-19 17:46:20 +00:00
}
type IDs []ID
// Lists all objects of a given type.
2014-08-04 20:15:04 +00:00
func (r *Repository) List(t Type) (IDs, error) {
// TODO: use os.Open() and d.Readdirnames() instead of Glob()
pattern := path.Join(r.dir(t), "*")
2014-04-19 17:37:34 +00:00
matches, err := filepath.Glob(pattern)
2014-04-19 17:37:34 +00:00
if err != nil {
return nil, err
2014-04-19 17:37:34 +00:00
}
ids := make(IDs, 0, len(matches))
2014-04-19 17:37:34 +00:00
for _, m := range matches {
base := filepath.Base(m)
2014-04-19 17:37:34 +00:00
if base == "" {
continue
}
id, err := ParseID(base)
if err != nil {
continue
}
ids = append(ids, id)
2014-04-19 17:37:34 +00:00
}
return ids, nil
2014-04-19 17:37:34 +00:00
}
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
2014-04-19 17:37:34 +00:00
}
for k, b := range ids[i] {
if b == ids[j][k] {
continue
}
2014-04-19 17:46:20 +00:00
if b < ids[j][k] {
return true
} else {
return false
}
2014-04-19 17:46:20 +00:00
}
2014-04-19 17:37:34 +00:00
return false
}
2014-04-19 17:37:34 +00:00
func (ids IDs) Swap(i, j int) {
ids[i], ids[j] = ids[j], ids[i]
2014-04-19 17:37:34 +00:00
}