Major refactoring of data formats to use protobuf instead of JSON. This is a breaking change to data formats, but hoping to stabilize soon.

This change removes lots of pointless string and custom format parsing code and instead relies on protobuf to do the thing. JSON is still an option thanks to proto3-generated output.

- Refactored ObjectID to use protobuf
- Indirect blocks to use protobuf (in the form of seek table)
- Added generic proto stream reader and writer
- ObjectIDFormat became proto enum

Also:

- fixed major issue where indirect object ID entries were not encrypted, since they were storing block IDs and not object IDs.
- dropped support for majority of formats, only supporting HMAC-{SHA256,SHA512,SHA512_384} hashes with AES256.
This produces object IDs of 32 or 64 characters long.
- changed how pretty-printing works for 'kopia show'
This commit is contained in:
Jarek Kowalski committed 2016-08-20 16:59:10 -07:00
1 parent 9c828db309
commit e528b32748
50 files changed
+1784 -1324

No files matched your search

+1
View File
@@ -3,3 +3,4 @@
.releases/
kopia-*.tar.gz
kopia-*.zip
combined.cov
+10 -1
View File
@@ -17,11 +17,20 @@ install:
build:
go build github.com/kopia/kopia/...
escape-analysis:
go build -gcflags '-m -l' github.com/kopia/kopia/...
protos:
protoc --go_out=../../.. fs/*.proto
protoc --go_out=../../.. repo/*.proto
protoc --go_out=../../.. vault/*.proto
protoc --go_out=../../.. internal/*.proto
play:
go run cmd/playground/main.go
lint:
golint github.com/kopia/kopia/...
-golint github.com/kopia/kopia/... | grep -v .pb.go
vet:
go tool vet -all .
+4 -4
View File
@@ -25,10 +25,10 @@ func (bg *backupGenerator) Backup(m *Manifest, old *Manifest) error {
uploader := fs.NewUploader(bg.repo)
m.StartTime = time.Now()
var hashCacheID repo.ObjectID
var hashCacheID *repo.ObjectID
if old != nil {
hashCacheID = repo.ObjectID(old.HashCacheID)
hashCacheID = &old.HashCacheID
}
entry, err := fs.NewFilesystemEntry(m.Source, nil)
@@ -46,8 +46,8 @@ func (bg *backupGenerator) Backup(m *Manifest, old *Manifest) error {
if err != nil {
return err
}
m.RootObjectID = string(r.ObjectID)
m.HashCacheID = string(r.ManifestID)
m.RootObjectID = r.ObjectID
m.HashCacheID = r.ManifestID
return nil
}
+6 -4
View File
@@ -5,6 +5,8 @@ import (
"encoding/hex"
"io"
"time"
"github.com/kopia/kopia/repo"
)
// Manifest stores information about single backup.
@@ -16,10 +18,10 @@ type Manifest struct {
UserName string `json:"userName"`
Description string `json:"description"`
Handle string `json:"handle"`
Source string `json:"source"`
RootObjectID string `json:"root"`
HashCacheID string `json:"hashCache"`
Handle string `json:"handle"`
Source string `json:"source"`
RootObjectID repo.ObjectID `json:"root"`
HashCacheID repo.ObjectID `json:"hashCache"`
FileCount int64 `json:"fileCount"`
DirectoryCount int64 `json:"dirCount"`
+1 -1
View File
@@ -113,7 +113,7 @@ func runBackupCommand(context *kingpin.ParseContext) error {
return fmt.Errorf("cannot save manifest: %v", err)
}
log.Printf("Root: %v", manifest.RootObjectID)
log.Printf("Root: %v", manifest.RootObjectID.UIString())
log.Printf("Key: %v", handleID)
}
+15 -10
View File
@@ -15,7 +15,7 @@ import (
var (
createCommand = app.Command("create", "Create new vault and repository.")
createCommandRepository = createCommand.Flag("repository", "Repository path.").Default("colocated").String()
createObjectFormat = createCommand.Flag("repo-format", "Format of repository objects.").PlaceHolder("FORMAT").Default("sha256-fold160-aes128").Enum(supportedObjectFormats()...)
createObjectFormat = createCommand.Flag("repo-format", "Format of repository objects.").PlaceHolder("FORMAT").Default(repo.DefaultObjectFormat.Name()).Enum(supportedObjectFormats()...)
createMaxBlobSize = createCommand.Flag("max-blob-size", "Maximum size of a data chunk.").PlaceHolder("KB").Default("20480").Int()
createInlineBlobSize = createCommand.Flag("inline-blob-size", "Maximum size of an inline data chunk.").PlaceHolder("KB").Default("32").Int()
@@ -43,15 +43,20 @@ func vaultFormat() (*vault.Format, error) {
}
func repositoryFormat() (*repo.Format, error) {
f := &repo.Format{
Version: "1",
Secret: make([]byte, 32),
MaxBlobSize: *createMaxBlobSize * 1024,
MaxInlineBlobSize: *createInlineBlobSize * 1024,
ObjectFormat: *createObjectFormat,
objectIDFormat, err := repo.ParseObjectIDFormat(*createObjectFormat)
if err != nil {
return nil, err
}
_, err := io.ReadFull(rand.Reader, f.Secret)
f := &repo.Format{
Version: 1,
Secret: make([]byte, 32),
MaxBlobSize: int32(*createMaxBlobSize * 1024),
MaxInlineBlobSize: int32(*createInlineBlobSize * 1024),
ObjectFormat: objectIDFormat,
}
_, err = io.ReadFull(rand.Reader, f.Secret)
if err != nil {
return nil, err
}
@@ -100,7 +105,7 @@ func runCreateCommand(context *kingpin.ParseContext) error {
}
fmt.Printf(
"Initializing repository in with format '%v' and maximum object size %v.\n",
"Initializing repository in with format %v and maximum object size %v.\n",
repoFormat.ObjectFormat,
repoFormat.MaxBlobSize)
@@ -151,7 +156,7 @@ func supportedVaultEncryptionFormats() []string {
func supportedObjectFormats() []string {
var r []string
for _, o := range repo.SupportedFormats {
r = append(r, o.Name)
r = append(r, o.Name())
}
return r
}
+12 -8
View File
@@ -6,7 +6,6 @@ import (
"strings"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/repo"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
@@ -69,17 +68,22 @@ func listDirectory(prefix string, entries fs.Entries, longFormat bool) {
var info string
if longFormat {
var oid string
if m.ObjectID.Type() == repo.ObjectIDTypeBinary {
oid = "<inline binary>"
} else if m.ObjectID.Type() == repo.ObjectIDTypeText {
oid = "<inline text>"
if m.ObjectId.Content != nil {
oid = "<inline content>"
} else {
oid = string(m.ObjectID)
oid = m.ObjectId.UIString()
}
info = fmt.Sprintf("%v %9d %v %-"+maxNameLenString+"s %v", m.FileMode, m.FileSize, m.ModTime.Local().Format("02 Jan 06 15:04:05"), m.Name, oid)
info = fmt.Sprintf(
"%v %9d %v %-"+maxNameLenString+"s %v",
m.FileMode(),
m.FileSize,
m.ModTime().Local().Format("02 Jan 06 15:04:05"),
m.Name,
oid,
)
} else {
var suffix string
if m.FileMode.IsDir() {
if m.FileMode().IsDir() {
suffix = "/"
}
+47 -2
View File
@@ -1,11 +1,19 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/golang/protobuf/proto"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/internal"
"github.com/kopia/kopia/repo"
kingpin "gopkg.in/alecthomas/kingpin.v2"
@@ -54,8 +62,8 @@ func showObject(mgr repo.Repository, oid repo.ObjectID) error {
format := "raw"
if len(rawdata) > 2 && rawdata[0] == '{' && rawdata[len(rawdata)-1] == '}' {
format = "json"
if len(rawdata) >= 8 {
format = detectProtoFormat(rawdata[0:8])
}
if *showJSON {
@@ -73,12 +81,49 @@ func showObject(mgr repo.Repository, oid repo.ObjectID) error {
json.Indent(&buf, rawdata, "", " ")
os.Stdout.Write(buf.Bytes())
case "dir":
return prettyPrintProtoStream(rawdata, internal.ProtoStreamTypeDir, &fs.EntryMetadata{})
case "indirect":
return prettyPrintProtoStream(rawdata, internal.ProtoStreamTypeIndirect, &repo.IndirectObjectEntry{})
case "hashcache":
return prettyPrintProtoStream(rawdata, internal.ProtoStreamTypeHashCache, &fs.HashCacheEntry{})
default:
os.Stdout.Write(rawdata)
}
return nil
}
func detectProtoFormat(b []byte) string {
switch {
case bytes.Equal(b, internal.ProtoStreamTypeDir):
return "dir"
case bytes.Equal(b, internal.ProtoStreamTypeIndirect):
return "indirect"
case bytes.Equal(b, internal.ProtoStreamTypeHashCache):
return "hashcache"
}
return "raw"
}
func prettyPrintProtoStream(data []byte, header []byte, pb proto.Message) error {
r := internal.NewProtoStreamReader(bufio.NewReader(bytes.NewReader(data)), header)
i := 0
for {
err := r.Read(pb)
if err == io.EOF {
return nil
}
if err != nil {
return err
}
fmt.Printf("[entry #%v]\n", i)
fmt.Println(proto.MarshalTextString(pb))
i++
}
}
func init() {
showCommand.Action(runShowCommand)
}
+8 -8
View File
@@ -13,7 +13,7 @@ import (
func parseObjectID(id string, vlt *vault.Vault) (repo.ObjectID, error) {
head, tail := splitHeadTail(id)
if len(head) == 0 {
return "", fmt.Errorf("invalid object ID: %v", id)
return repo.NullObjectID, fmt.Errorf("invalid object ID: %v", id)
}
if !strings.HasPrefix(id, vault.StoredObjectIDPrefix) {
@@ -22,7 +22,7 @@ func parseObjectID(id string, vlt *vault.Vault) (repo.ObjectID, error) {
oid, err := vlt.GetObjectID(head)
if err != nil {
return "", fmt.Errorf("can't retrieve vault object ID %v: %v", head, err)
return repo.NullObjectID, fmt.Errorf("can't retrieve vault object ID %v: %v", head, err)
}
if tail == "" {
@@ -31,12 +31,12 @@ func parseObjectID(id string, vlt *vault.Vault) (repo.ObjectID, error) {
r, err := vlt.OpenRepository()
if err != nil {
return "", fmt.Errorf("cannot open repository: %v", err)
return repo.NullObjectID, fmt.Errorf("cannot open repository: %v", err)
}
dir := fs.NewRepositoryDirectory(r, oid)
if err != nil {
return "", err
return repo.NullObjectID, err
}
return parseNestedObjectID(dir, tail)
@@ -49,24 +49,24 @@ func parseNestedObjectID(startingDir fs.Directory, id string) (repo.ObjectID, er
for head != "" {
dir, ok := current.(fs.Directory)
if !ok {
return "", fmt.Errorf("entry not found '%v': parent is not a directory", head)
return repo.NullObjectID, fmt.Errorf("entry not found '%v': parent is not a directory", head)
}
entries, err := dir.Readdir()
if err != nil {
return "", err
return repo.NullObjectID, err
}
e := entries.FindByName(head)
if e == nil {
return "", fmt.Errorf("entry not found: '%v'", head)
return repo.NullObjectID, fmt.Errorf("entry not found: '%v'", head)
}
current = e
head, tail = splitHeadTail(tail)
}
return current.Metadata().ObjectID, nil
return *current.Metadata().ObjectId, nil
}
func splitHeadTail(id string) (string, string) {
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"bufio"
"bytes"
"log"
"github.com/kopia/kopia/internal"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/storage/storagetesting"
)
func dumpListBlock(prefix string, r repo.Repository, oid repo.ObjectID) {
ind := oid.Indirect
oid.Indirect = 0
log.Printf("%vdumping %v", prefix, oid.String())
rd, err := r.Open(oid)
if err != nil {
log.Fatalf("unable to open %v: %v", oid.String(), err)
}
defer rd.Close()
var objects []repo.ObjectID
pr := internal.NewProtoStreamReader(bufio.NewReader(rd), internal.ProtoStreamTypeIndirect)
for {
var v repo.IndirectObjectEntry
if err := pr.Read(&v); err != nil {
log.Printf("err: %v", err)
break
}
log.Printf("%v [%v] entry: %v", prefix, oid.String(), v.String())
if ind > 0 {
objects = append(objects, *v.Object)
}
}
for _, o := range objects {
dumpListBlock(prefix+" ", r, o)
}
}
func main() {
d := map[string][]byte{}
s := storagetesting.NewMapStorage(d)
f := repo.Format{
Version: 1,
MaxBlobSize: 100,
ObjectFormat: repo.ObjectIDFormat_TESTONLY_MD5,
}
r, err := repo.NewRepository(s, &f)
if err != nil {
log.Fatalf("Err: %v", err)
}
w := r.NewWriter(repo.WithDescription("tester"))
w.Write(bytes.Repeat([]byte("a"), 5000))
result, err := w.Result(true)
log.Printf("result: %v", result.UIString())
//dumpListBlock("", r, result)
log.Printf("attempting to open")
rdr, err := r.Open(result)
if err != nil {
log.Fatalf("open: %v", err)
}
log.Printf("len: %v\n", rdr.Length())
log.Printf("----")
// for k, v := range d {
// log.Printf("%v = %v", k, string(v))
// }
rdr.Close()
}
+36 -364
View File
@@ -1,407 +1,79 @@
package fs
import (
"encoding/json"
"bufio"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"github.com/kopia/kopia/internal"
"github.com/kopia/kopia/repo"
)
const modeChars = "dalTLDpSugct"
type jsonDirectoryEntry struct {
Name string `json:"name"`
Type string `json:"type,omitempty"`
Permissions string `json:"perm,omitempty"`
Size string `json:"size,omitempty"`
Time *time.Time `json:"mtime,omitempty"`
Owner string `json:"owner,omitempty"`
ObjectID string `json:"oid,omitempty"`
JSONContent json.RawMessage `json:"content,omitempty"`
SubEntries []jsonDirectoryEntry `json:"entries,omitempty"`
}
func (em *EntryMetadata) fromJSON(jde *jsonDirectoryEntry) error {
if jde.Name == "" {
return fmt.Errorf("empty entry name")
}
em.Name = jde.Name
if jde.Permissions != "" {
if m, err := parseFilePermissions(jde.Permissions); err == nil {
em.FileMode |= m
} else {
return fmt.Errorf("invalid permissions: '%v'", jde.Permissions)
}
}
if jde.Type != "" {
if m, err := parseFileMode(jde.Type); err == nil {
em.FileMode |= m
} else {
return fmt.Errorf("invalid type: '%v'", jde.Type)
}
}
if jde.Time != nil {
em.ModTime = *jde.Time
}
if jde.Owner != "" {
if c, err := fmt.Sscanf(jde.Owner, "%d:%d", &em.OwnerID, &em.GroupID); err != nil || c != 2 {
return fmt.Errorf("invalid owner: %v", err)
}
}
if jde.JSONContent != nil {
em.ObjectID = repo.NewInlineObjectID([]byte(jde.JSONContent))
} else {
em.ObjectID = repo.ObjectID(jde.ObjectID)
}
if jde.Size != "" {
if s, err := strconv.ParseInt(jde.Size, 10, 64); err == nil {
em.FileSize = s
} else {
return fmt.Errorf("invalid size: %v", err)
}
}
return nil
}
// parseFileModeAndPermissions converts file mode string to os.FileMode
func parseFileModeAndPermissions(s string) (os.FileMode, error) {
colon := strings.IndexByte(s, ':')
if colon < 0 {
return parseFilePermissions(s)
}
var mode os.FileMode
if m, err := parseFileMode(s[0:colon]); err == nil {
mode |= m
} else {
return 0, err
}
if m, err := parseFilePermissions(s[colon+1:]); err == nil {
mode |= m
} else {
return 0, err
}
return mode, nil
}
func parseFileMode(s string) (os.FileMode, error) {
var mode os.FileMode
for _, c := range s {
switch c {
case 'd':
mode |= os.ModeDir
case 'a':
mode |= os.ModeAppend
case 'l':
mode |= os.ModeExclusive
case 'T':
mode |= os.ModeTemporary
case 'L':
mode |= os.ModeSymlink
case 'D':
mode |= os.ModeDevice
case 'p':
mode |= os.ModeNamedPipe
case 'S':
mode |= os.ModeSocket
case 'u':
mode |= os.ModeSetuid
case 'g':
mode |= os.ModeSetgid
case 'c':
mode |= os.ModeCharDevice
case 't':
mode |= os.ModeSticky
default:
return 0, fmt.Errorf("unsupported mode: '%v'", c)
}
}
return mode, nil
}
func parseFilePermissions(perm string) (os.FileMode, error) {
s, err := strconv.ParseUint(perm, 8, 32)
if err != nil {
return 0, err
}
return os.FileMode(s), nil
}
type directoryWriter struct {
io.Closer
writer io.Writer
buf []byte
separator []byte
lastNameWritten string
w *internal.ProtoStreamWriter
}
func (dw *directoryWriter) WriteEntry(e *EntryMetadata, children []*EntryMetadata) error {
if dw.lastNameWritten != "" {
if isLessOrEqual(e.Name, dw.lastNameWritten) {
return fmt.Errorf("out-of-order directory entry, previous '%v' current '%v'", dw.lastNameWritten, e.Name)
}
dw.lastNameWritten = e.Name
}
jde := e.toJSONEntry()
if len(children) > 0 {
jde.SubEntries = make([]jsonDirectoryEntry, len(children))
for i, se := range children {
jde.SubEntries[i] = se.toJSONEntry()
}
}
v, _ := json.Marshal(&jde)
dw.writer.Write(dw.separator)
dw.writer.Write(v)
dw.separator = []byte(",")
return nil
return dw.w.Write(e)
}
func (em *EntryMetadata) toJSONEntry() jsonDirectoryEntry {
var jde jsonDirectoryEntry
jde.Name = em.Name
if (em.FileMode & os.ModeType) != 0 {
jde.Type = formatMode(em.FileMode)
}
if (em.FileMode & os.ModePerm) != 0 {
jde.Permissions = formatPermissions(em.FileMode)
}
if em.OwnerID > 0 || em.GroupID > 0 {
jde.Owner = fmt.Sprintf("%d:%d", em.OwnerID, em.GroupID)
}
if !em.ModTime.IsZero() {
utc := em.ModTime.UTC()
jde.Time = &utc
}
if em.ObjectID != "" {
inline := em.ObjectID.InlineData()
if len(inline) >= 2 && inline[0] == '{' && inline[len(inline)-1] == '}' {
m := map[string]interface{}{}
if json.Unmarshal(inline, &m) == nil {
jde.JSONContent = json.RawMessage(inline)
}
}
}
if jde.JSONContent == nil {
jde.ObjectID = string(em.ObjectID)
}
if em.FileMode.IsRegular() {
jde.Size = strconv.FormatInt(em.FileSize, 10)
}
return jde
}
func formatMode(m os.FileMode) string {
const str = "dalTLDpSugct"
var buf [32]byte
w := 0
for i, c := range str {
if m&(1<<uint(32-1-i)) != 0 {
buf[w] = byte(c)
w++
}
}
return string(buf[:w])
}
func formatPermissions(m os.FileMode) string {
return strconv.FormatInt(int64(m&os.ModePerm), 8)
}
func (dw *directoryWriter) Close() error {
dw.writer.Write([]byte("]}"))
return nil
}
func newDirectoryWriter(w io.Writer) *directoryWriter {
func newDirectoryWriter(w io.WriteCloser) *directoryWriter {
dw := &directoryWriter{
writer: w,
w: internal.NewProtoStreamWriter(w, internal.ProtoStreamTypeDir),
}
var f directoryFormat
f.Version = 1
io.WriteString(w, "{\"format\":")
b, _ := json.Marshal(&f)
w.Write(b)
io.WriteString(w, ",\"entries\":[")
dw.separator = []byte("")
return dw
}
type directoryReader struct {
reader io.Reader
decoder *json.Decoder
}
func (dr *directoryReader) readNext(jde *jsonDirectoryEntry) error {
if dr.decoder.More() {
return dr.decoder.Decode(&jde)
}
if err := ensureDelimiter(dr.decoder, json.Delim(']')); err != nil {
return invalidDirectoryError(err)
}
if err := ensureDelimiter(dr.decoder, json.Delim('}')); err != nil {
return invalidDirectoryError(err)
}
return io.EOF
}
func invalidDirectoryError(cause error) error {
return fmt.Errorf("invalid directory data: %v", cause)
}
type directoryFormat struct {
Version int `json:"version"`
}
func ensureDelimiter(d *json.Decoder, expected json.Delim) error {
t, err := d.Token()
if err != nil {
return err
}
if t != expected {
return fmt.Errorf("expected '%v', got %v", expected.String(), t)
}
return nil
}
func ensureStringToken(d *json.Decoder, expected string) error {
t, err := d.Token()
if err != nil {
return err
}
if s, ok := t.(string); ok {
if s == expected {
return nil
}
}
return fmt.Errorf("expected '%v', got '%v'", expected, t)
}
func newDirectoryReader(r io.Reader) (*directoryReader, error) {
dr := &directoryReader{
decoder: json.NewDecoder(r),
}
if err := ensureDelimiter(dr.decoder, json.Delim('{')); err != nil {
return nil, invalidDirectoryError(err)
}
if err := ensureStringToken(dr.decoder, "format"); err != nil {
return nil, invalidDirectoryError(err)
}
// Parse format and trailing comma
var format directoryFormat
if err := dr.decoder.Decode(&format); err != nil {
return nil, invalidDirectoryError(err)
}
if format.Version != 1 {
return nil, invalidDirectoryError(fmt.Errorf("unsupported version: %v", format.Version))
}
if err := ensureStringToken(dr.decoder, "entries"); err != nil {
return nil, invalidDirectoryError(err)
}
if err := ensureDelimiter(dr.decoder, json.Delim('[')); err != nil {
return nil, invalidDirectoryError(err)
}
return dr, nil
}
func readDirectoryMetadataEntries(r io.Reader) ([]*EntryMetadata, error) {
dr, err := newDirectoryReader(r)
if err != nil {
return nil, err
psr := internal.NewProtoStreamReader(bufio.NewReader(r), internal.ProtoStreamTypeDir)
var entries []*EntryMetadata
for {
e := &EntryMetadata{}
err := psr.Read(e)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
entries = append(entries, e)
}
return flattenBundles(entries)
}
func flattenBundles(source []*EntryMetadata) ([]*EntryMetadata, error) {
var entries []*EntryMetadata
var bundles [][]*EntryMetadata
for {
var e jsonDirectoryEntry
if err := dr.readNext(&e); err != nil {
if err == io.EOF {
break
}
return nil, err
}
var entryMetadata EntryMetadata
if err := entryMetadata.fromJSON(&e); err != nil {
return nil, err
}
if len(e.SubEntries) > 0 {
bundle := make([]*EntryMetadata, 0, len(e.SubEntries))
for _, e := range source {
if len(e.BundledChildren) > 0 {
bundle := e.BundledChildren
e.BundledChildren = nil
var currentOffset int64
var bundleEntry EntryMetadata
if err := bundleEntry.fromJSON(&e); err != nil {
return nil, err
for _, child := range bundle {
id := repo.NewSectionObjectID(currentOffset, child.FileSize, e.ObjectId)
child.ObjectId = &id
currentOffset += child.FileSize
}
for _, s := range e.SubEntries {
var subEntry EntryMetadata
if err := subEntry.fromJSON(&s); err != nil {
return nil, err
}
subEntry.ObjectID = repo.NewSectionObjectID(currentOffset, subEntry.FileSize, entryMetadata.ObjectID)
currentOffset += subEntry.FileSize
bundle = append(bundle, &subEntry)
}
if currentOffset != entryMetadata.FileSize {
return nil, fmt.Errorf("inconsistent size of '%v': %v (got %v)", entryMetadata.Name, entryMetadata.FileSize, currentOffset)
if currentOffset != e.FileSize {
return nil, fmt.Errorf("inconsistent size of '%v': %v (got %v)", e.Name, e.FileSize, currentOffset)
}
bundles = append(bundles, bundle)
} else {
entries = append(entries, &entryMetadata)
entries = append(entries, e)
}
}
+134 -158
View File
@@ -1,11 +1,12 @@
package fs
import (
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/kopia/kopia/repo"
)
var (
@@ -14,147 +15,143 @@ var (
time3 = mustParseTimestamp("2016-04-02T02:36:19Z")
)
func TestEmptyDirectory(t *testing.T) {
data := strings.Join(
[]string{
`{`,
`"format":{"version":1},`,
`"entries":[]`,
`}`,
}, "")
expectedEntries := []*EntryMetadata{}
verifyDirectory(t, data, expectedEntries)
}
func TestDirectoryWithOnlyBundle(t *testing.T) {
data := strings.Join(
[]string{
`{`,
`"format":{"version":1},`,
`"entries":[`,
`{"name":"bundle1","size":"170","perm":"0","oid":"D5555","entries":[`,
`{"name":"a1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"50"},`,
`{"name":"z1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"120"}`,
`]}`,
`]}`,
}, "")
expectedEntries := []*EntryMetadata{
&EntryMetadata{Name: "a1", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 50, ObjectID: "S0,50,D5555"},
&EntryMetadata{Name: "z1", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 120, ObjectID: "S50,120,D5555"},
func TestFlattenBundles(t *testing.T) {
base := &repo.ObjectID{StorageBlock: "5555"}
sources := []*EntryMetadata{
&EntryMetadata{
Name: "bundle1",
FileSize: 170,
ObjectId: base,
BundledChildren: []*EntryMetadata{
&EntryMetadata{Name: "a1", FileSize: 50},
&EntryMetadata{Name: "z1", FileSize: 120},
},
},
}
verifyDirectory(t, data, expectedEntries)
}
func TestInconsistentBundleSize(t *testing.T) {
data := strings.Join(
[]string{
`{`,
`"format":{"version":1},`,
`"entries":[`,
`{"name":"bundle1","size":"170","perm":"0","oid":"D5555","entries":[`,
`{"name":"a1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"51"},`,
`{"name":"z1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"120"}`,
`]}`,
`]}`,
}, "")
verifyDirectoryError(t, data, "inconsistent size of 'bundle1': 170 (got 171)")
}
func TestInvalidBundleHeaderData(t *testing.T) {
data := strings.Join(
[]string{
`{`,
`"format":{"version":1},`,
`"entries":[`,
`{"name":"bundle1","size":"170","perm":"x","oid":"D5555","entries":[`,
`{"name":"z1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"120"}`,
`]}`,
`]}`,
}, "")
verifyDirectoryError(t, data, "invalid permissions: 'x'")
}
func TestInvalidBundleEntryData(t *testing.T) {
data := strings.Join(
[]string{
`{`,
`"format":{"version":1},`,
`"entries":[`,
`{"name":"bundle1","size":"170","perm":"0","oid":"D5555","entries":[`,
`{"perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"51"},`,
`{"name":"z1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"120"}`,
`]}`,
`]}`,
}, "")
verifyDirectoryError(t, data, "empty entry name")
}
func TestDirectoryWithoutBundle(t *testing.T) {
data := strings.Join(
[]string{
`{`,
`"format":{"version":1},`,
`"entries":[`,
`{"name":"constants.go","perm":"420","size":"13","mtime":"2016-04-02T02:36:19Z","owner":"500:100","oid":"D5123"}`,
`]}`,
}, "")
expectedEntries := []*EntryMetadata{
&EntryMetadata{Name: "constants.go", FileMode: 0420, ModTime: time3, OwnerID: 500, GroupID: 100, FileSize: 13, ObjectID: "D5123"},
}
verifyDirectory(t, data, expectedEntries)
}
func TestDirectoryWithThreeBundles(t *testing.T) {
data := strings.Join(
[]string{
`{`,
`"format":{"version":1},`,
`"entries":[`,
`{"name":"bundle1","size":"170","perm":"0","oid":"D5555","entries":[`,
`{"name":"a1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"50"},`,
`{"name":"z1","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"120"}`,
`]},`,
`{"name":"config.go","perm":"420","size":"937","mtime":"2016-04-02T02:39:44.123456789Z","owner":"500:100","oid":"D4321"},`,
`{"name":"constants.go","perm":"420","size":"13","mtime":"2016-04-02T02:36:19Z","owner":"500:100","oid":"D5123"},`,
`{"name":"subdir","type":"d","perm":"755","mtime":"2016-04-06T02:34:10Z","owner":"500:100","oid":"D1234"},`,
`{"name":"bundle3","size":"7","perm":"0","oid":"D8888","entries":[`,
`{"name":"a3","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"5"},`,
`{"name":"z3","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"2"}`,
`]},`,
`{"name":"bundle2","size":"170","perm":"0","oid":"D6666","entries":[`,
`{"name":"a2","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"150"},`,
`{"name":"z2","perm":"500","mtime":"2016-04-06T02:34:10Z","owner":"500:100","size":"20"}`,
`]}`,
`]}`,
}, "")
expectedEntries := []*EntryMetadata{
&EntryMetadata{Name: "a1", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 50, ObjectID: "S0,50,D5555"},
&EntryMetadata{Name: "a2", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 150, ObjectID: "S0,150,D6666"},
&EntryMetadata{Name: "a3", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 5, ObjectID: "S0,5,D8888"},
&EntryMetadata{Name: "config.go", FileMode: 0420, ModTime: time2, OwnerID: 500, GroupID: 100, FileSize: 937, ObjectID: "D4321"},
&EntryMetadata{Name: "constants.go", FileMode: 0420, ModTime: time3, OwnerID: 500, GroupID: 100, FileSize: 13, ObjectID: "D5123"},
&EntryMetadata{Name: "subdir", FileMode: os.ModeDir | 0755, ModTime: time1, OwnerID: 500, GroupID: 100, ObjectID: "D1234"},
&EntryMetadata{Name: "z1", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 120, ObjectID: "S50,120,D5555"},
&EntryMetadata{Name: "z2", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 20, ObjectID: "S150,20,D6666"},
&EntryMetadata{Name: "z3", FileMode: 0500, ModTime: time1, OwnerID: 500, GroupID: 100, FileSize: 2, ObjectID: "S5,2,D8888"},
}
verifyDirectory(t, data, expectedEntries)
}
func verifyDirectory(t *testing.T, data string, expectedEntries []*EntryMetadata) {
entries, err := readDirectoryMetadataEntries(strings.NewReader(data))
entries, err := flattenBundles(sources)
if err != nil {
t.Errorf("can't read directory entries: %v", err)
return
}
expectedEntries := []*EntryMetadata{
&EntryMetadata{Name: "a1", FileSize: 50, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 0,
Length: 50,
Base: base,
}}},
&EntryMetadata{Name: "z1", FileSize: 120, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 50,
Length: 120,
Base: base,
}}},
}
verifyDirectory(t, entries, expectedEntries)
}
func TestFlattenBundlesInconsistentBundleSize(t *testing.T) {
sources := []*EntryMetadata{
&EntryMetadata{
Name: "bundle1",
FileSize: 171,
ObjectId: &repo.ObjectID{StorageBlock: "5555"},
BundledChildren: []*EntryMetadata{
&EntryMetadata{Name: "a1", FileSize: 50},
&EntryMetadata{Name: "z1", FileSize: 120},
},
},
}
_, err := flattenBundles(sources)
if err == nil {
t.Errorf("expected error")
return
}
if ok := strings.Contains(err.Error(), "inconsistent size of 'bundle1'"); !ok {
t.Errorf("invalid error: %v", err)
}
}
func TestFlattenThreeBundles(t *testing.T) {
base1 := &repo.ObjectID{StorageBlock: "5555"}
base2 := &repo.ObjectID{StorageBlock: "6666"}
base3 := &repo.ObjectID{StorageBlock: "7777"}
sources := []*EntryMetadata{
&EntryMetadata{
Name: "bundle1",
FileSize: 170,
ObjectId: base1,
BundledChildren: []*EntryMetadata{
&EntryMetadata{Name: "a1", FileSize: 50},
&EntryMetadata{Name: "z1", FileSize: 120},
},
},
&EntryMetadata{
Name: "bundle3",
FileSize: 7,
ObjectId: base3,
BundledChildren: []*EntryMetadata{
&EntryMetadata{Name: "a3", FileSize: 5},
&EntryMetadata{Name: "z3", FileSize: 2},
},
},
&EntryMetadata{
Name: "bundle2",
FileSize: 300,
ObjectId: base2,
BundledChildren: []*EntryMetadata{
&EntryMetadata{Name: "a2", FileSize: 100},
&EntryMetadata{Name: "z2", FileSize: 200},
},
},
}
entries, err := flattenBundles(sources)
if err != nil {
t.Errorf("can't read directory entries: %v", err)
return
}
expectedEntries := []*EntryMetadata{
&EntryMetadata{Name: "a1", FileSize: 50, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 0,
Length: 50,
Base: base1,
}}},
&EntryMetadata{Name: "a2", FileSize: 100, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 0,
Length: 100,
Base: base2,
}}},
&EntryMetadata{Name: "a3", FileSize: 5, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 0,
Length: 5,
Base: base3,
}}},
&EntryMetadata{Name: "z1", FileSize: 120, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 50,
Length: 120,
Base: base1,
}}},
&EntryMetadata{Name: "z2", FileSize: 200, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 100,
Length: 200,
Base: base2,
}}},
&EntryMetadata{Name: "z3", FileSize: 2, ObjectId: &repo.ObjectID{Section: &repo.ObjectID_Section{
Start: 5,
Length: 2,
Base: base3,
}}},
}
verifyDirectory(t, entries, expectedEntries)
}
func verifyDirectory(t *testing.T, entries []*EntryMetadata, expectedEntries []*EntryMetadata) {
if len(entries) != len(expectedEntries) {
t.Errorf("expected %v entries, got %v", len(expectedEntries), len(entries))
}
@@ -164,34 +161,13 @@ func verifyDirectory(t *testing.T, data string, expectedEntries []*EntryMetadata
actual := entries[i]
if !reflect.DeepEqual(expected, actual) {
t.Errorf("invalid entry at index %v:\nexpected: %#v\nactual: %#v", i, expected, actual)
t.Errorf("invalid entry at index %v:\nexpected: %v\nactual: %v", i,
expected.String(), actual.String())
}
}
}
}
func TestInvalidJSON(t *testing.T) {
verifyDirectoryError(t, "{invalid", "invalid character 'i'")
verifyDirectoryError(t, `{"format":{"version":1},"entries":[{"name":""}]}`, "empty entry name")
verifyDirectoryError(t, `{"format":{"version":1},"entries":[{"name":"x","perm":"x123"}]}`, "invalid permissions: 'x123'")
verifyDirectoryError(t, `{"format":{"version":2},"entries":[]}`, "unsupported version: 2")
}
func verifyDirectoryError(t *testing.T, data, expectedError string) {
entries, err := readDirectoryMetadataEntries(strings.NewReader(data))
if err == nil {
t.Errorf("expected error %v, got no error", expectedError)
} else {
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error containing '%v', got '%v'", expectedError, err.Error())
}
}
if entries != nil {
t.Errorf("got unexpected result: %#v", entries)
}
}
func TestDirectoryNameOrder(t *testing.T) {
sortedNames := []string{
"a/a/a",
+16 -20
View File
@@ -8,8 +8,6 @@ import (
"sort"
"strings"
"time"
"github.com/kopia/kopia/repo"
)
// Entry represents a filesystem entry, which can be Directory, File, or Symlink
@@ -45,17 +43,6 @@ type Symlink interface {
Readlink() (string, error)
}
// EntryMetadata stores attributes of a single entry in a directory.
type EntryMetadata struct {
Name string
FileMode os.FileMode
FileSize int64
ModTime time.Time
OwnerID uint32
GroupID uint32
ObjectID repo.ObjectID
}
type entry struct {
parent Directory
metadata *EntryMetadata
@@ -133,15 +120,24 @@ func isLessOrEqual(name1, name2 string) bool {
return len(parts1) <= len(parts2)
}
// FileMode returns the file mode and permissions.
func (e *EntryMetadata) FileMode() os.FileMode {
return os.FileMode(e.ModeAndPermissions)
}
// ModTime returns the modification time.
func (e *EntryMetadata) ModTime() time.Time {
return time.Unix(e.ModTimestampNano/1000000000, e.ModTimestampNano%1000000000)
}
func (e *EntryMetadata) metadataHash() uint64 {
h := fnv.New64a()
binary.Write(h, binary.LittleEndian, e.ModTime.UnixNano())
binary.Write(h, binary.LittleEndian, e.FileMode)
if e.FileMode.IsRegular() {
binary.Write(h, binary.LittleEndian, e.FileSize)
}
binary.Write(h, binary.LittleEndian, e.OwnerID)
binary.Write(h, binary.LittleEndian, e.GroupID)
binary.Write(h, binary.LittleEndian, e.ModTimestampNano)
binary.Write(h, binary.LittleEndian, e.ModeAndPermissions)
binary.Write(h, binary.LittleEndian, e.FileSize)
binary.Write(h, binary.LittleEndian, e.Uid)
binary.Write(h, binary.LittleEndian, e.Gid)
return h.Sum64()
}
+89
View File
@@ -0,0 +1,89 @@
// Code generated by protoc-gen-go.
// source: fs/entry_metadata.proto
// DO NOT EDIT!
/*
Package fs is a generated protocol buffer package.
It is generated from these files:
fs/entry_metadata.proto
fs/hashcache.proto
It has these top-level messages:
EntryMetadata
HashCacheEntry
*/
package fs
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import kopia_repo "github.com/kopia/kopia/repo"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
const _ = proto.ProtoPackageIsVersion1
// EntryMetadata stores attributes of a single entry in a directory.
type EntryMetadata struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
ModeAndPermissions uint32 `protobuf:"fixed32,2,opt,name=mode_and_permissions,json=modeAndPermissions" json:"mode_and_permissions,omitempty"`
FileSize int64 `protobuf:"varint,3,opt,name=file_size,json=fileSize" json:"file_size,omitempty"`
ModTimestampNano int64 `protobuf:"fixed64,4,opt,name=mod_timestamp_nano,json=modTimestampNano" json:"mod_timestamp_nano,omitempty"`
ObjectId *kopia_repo.ObjectID `protobuf:"bytes,5,opt,name=object_id,json=objectId" json:"object_id,omitempty"`
Uid uint32 `protobuf:"varint,6,opt,name=uid" json:"uid,omitempty"`
Gid uint32 `protobuf:"varint,7,opt,name=gid" json:"gid,omitempty"`
BundledChildren []*EntryMetadata `protobuf:"bytes,8,rep,name=bundled_children,json=bundledChildren" json:"bundled_children,omitempty"`
}
func (m *EntryMetadata) Reset() { *m = EntryMetadata{} }
func (m *EntryMetadata) String() string { return proto.CompactTextString(m) }
func (*EntryMetadata) ProtoMessage() {}
func (*EntryMetadata) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *EntryMetadata) GetObjectId() *kopia_repo.ObjectID {
if m != nil {
return m.ObjectId
}
return nil
}
func (m *EntryMetadata) GetBundledChildren() []*EntryMetadata {
if m != nil {
return m.BundledChildren
}
return nil
}
func init() {
proto.RegisterType((*EntryMetadata)(nil), "kopia.fs.EntryMetadata")
}
var fileDescriptor0 = []byte{
// 306 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x54, 0x90, 0xcd, 0x4e, 0x02, 0x31,
0x14, 0x85, 0x03, 0x83, 0x30, 0xd4, 0x10, 0x27, 0x95, 0x84, 0x2a, 0x1b, 0xe2, 0xca, 0x85, 0x29,
0xfe, 0x3c, 0x81, 0xa8, 0x0b, 0x16, 0xfe, 0xa4, 0xba, 0x72, 0xd3, 0x74, 0x68, 0x07, 0xaa, 0xb4,
0x9d, 0x4c, 0xcb, 0x42, 0x5f, 0xcd, 0x97, 0xf3, 0x76, 0x66, 0x88, 0x71, 0xd3, 0xdc, 0x9e, 0xf3,
0x9d, 0xa6, 0xf7, 0xa0, 0x49, 0xe1, 0xe7, 0xca, 0x86, 0xea, 0x8b, 0x1b, 0x15, 0x84, 0x14, 0x41,
0xd0, 0xb2, 0x72, 0xc1, 0xe1, 0xf4, 0xd3, 0x95, 0x5a, 0xd0, 0xc2, 0x9f, 0x1e, 0x57, 0xaa, 0x74,
0x73, 0x97, 0x7f, 0xa8, 0x55, 0xd0, 0xb2, 0xb1, 0xcf, 0x7e, 0xba, 0x68, 0xf4, 0x10, 0x73, 0x8f,
0x6d, 0x0c, 0x63, 0xd4, 0xb3, 0xc2, 0x28, 0xd2, 0x99, 0x75, 0xce, 0x87, 0xac, 0x9e, 0xf1, 0x25,
0x1a, 0x1b, 0x27, 0x15, 0x17, 0x56, 0xf2, 0x52, 0x55, 0x46, 0x7b, 0xaf, 0x9d, 0xf5, 0xa4, 0x0b,
0xcc, 0x80, 0xe1, 0xe8, 0xdd, 0x5a, 0xf9, 0xf2, 0xe7, 0xe0, 0x29, 0x1a, 0x16, 0x7a, 0xab, 0xb8,
0xd7, 0xdf, 0x8a, 0x24, 0x80, 0x25, 0x2c, 0x8d, 0xc2, 0x2b, 0xdc, 0xf1, 0x05, 0x8a, 0x11, 0x1e,
0xb4, 0x51, 0x3e, 0x08, 0x53, 0x72, 0x2b, 0xac, 0x23, 0x3d, 0xa0, 0x32, 0x96, 0x81, 0xf3, 0xb6,
0x37, 0x9e, 0x40, 0xc7, 0x57, 0x68, 0xd8, 0x7c, 0x9a, 0x6b, 0x49, 0x0e, 0x00, 0x3a, 0xbc, 0x1e,
0xd3, 0x66, 0xab, 0xb8, 0x11, 0x7d, 0xae, 0xcd, 0xe5, 0x3d, 0x4b, 0x1b, 0x6c, 0x29, 0x71, 0x86,
0x92, 0x1d, 0xc0, 0x7d, 0x80, 0x47, 0x2c, 0x8e, 0x51, 0x59, 0x83, 0x32, 0x68, 0x14, 0x18, 0xf1,
0x02, 0x65, 0xf9, 0xce, 0xca, 0xad, 0x92, 0x7c, 0xb5, 0xd1, 0x5b, 0x59, 0x29, 0x4b, 0xd2, 0x59,
0x02, 0xaf, 0x4f, 0xe8, 0xbe, 0x33, 0xfa, 0xaf, 0x1a, 0x76, 0xd4, 0x06, 0xee, 0x5a, 0x7e, 0x31,
0x7d, 0x3f, 0x59, 0xeb, 0xb0, 0xd9, 0xe5, 0x74, 0xe5, 0xcc, 0xbc, 0x4e, 0xb5, 0x67, 0xe1, 0xf3,
0x7e, 0xdd, 0xf0, 0xcd, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xcf, 0xf4, 0xc2, 0x88, 0x9b, 0x01,
0x00, 0x00,
}
+19
View File
@@ -0,0 +1,19 @@
syntax = "proto3";
package kopia.fs;
option go_package = "github.com/kopia/kopia/fs";
import "repo/objectid.proto";
// EntryMetadata stores attributes of a single entry in a directory.
message EntryMetadata {
string name = 1;
fixed32 mode_and_permissions = 2;
int64 file_size = 3;
sfixed64 mod_timestamp_nano = 4;
repo.ObjectID object_id = 5;
uint32 uid = 6;
uint32 gid = 7;
repeated EntryMetadata bundled_children = 8;
}
+17 -43
View File
@@ -4,36 +4,28 @@ import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/internal"
)
type hashCacheEntry struct {
Name string
Hash uint64
ObjectID repo.ObjectID
}
type hashcacheReader struct {
scanner *bufio.Scanner
nextEntry *hashCacheEntry
entry0 hashCacheEntry
entry1 hashCacheEntry
reader *internal.ProtoStreamReader
nextEntry *HashCacheEntry
entry0 HashCacheEntry
entry1 HashCacheEntry
odd bool
first bool
}
func (hcr *hashcacheReader) open(r io.Reader) error {
hcr.scanner = bufio.NewScanner(r)
hcr.reader = internal.NewProtoStreamReader(bufio.NewReader(r), internal.ProtoStreamTypeHashCache)
hcr.nextEntry = nil
hcr.first = true
hcr.readahead()
return nil
}
func (hcr *hashcacheReader) findEntry(relativeName string) *hashCacheEntry {
func (hcr *hashcacheReader) findEntry(relativeName string) *HashCacheEntry {
for hcr.nextEntry != nil && isLess(hcr.nextEntry.Name, relativeName) {
hcr.readahead()
}
@@ -49,37 +41,20 @@ func (hcr *hashcacheReader) findEntry(relativeName string) *hashCacheEntry {
}
func (hcr *hashcacheReader) readahead() {
if hcr.scanner != nil {
if hcr.first {
hcr.first = false
if !hcr.scanner.Scan() || hcr.scanner.Text() != "HASHCACHE:v1" {
hcr.scanner = nil
return
}
}
if hcr.reader != nil {
hcr.nextEntry = nil
if hcr.scanner.Scan() {
var err error
e := hcr.nextManifestEntry()
t := hcr.scanner.Text()
p0 := strings.IndexByte(t, '|')
p1 := strings.LastIndexByte(t, '|')
e.Name = t[0:p0]
e.Hash, err = strconv.ParseUint(t[p0+1:p1], 0, 64)
e.ObjectID = repo.ObjectID(t[p1+1:])
if err == nil {
hcr.nextEntry = e
}
e := hcr.nextManifestEntry()
if err := hcr.reader.Read(e); err == nil {
hcr.nextEntry = e
}
}
if hcr.nextEntry == nil {
hcr.scanner = nil
hcr.reader = nil
}
}
func (hcr *hashcacheReader) nextManifestEntry() *hashCacheEntry {
func (hcr *hashcacheReader) nextManifestEntry() *HashCacheEntry {
hcr.odd = !hcr.odd
if hcr.odd {
return &hcr.entry1
@@ -89,19 +64,18 @@ func (hcr *hashcacheReader) nextManifestEntry() *hashCacheEntry {
}
type hashcacheWriter struct {
writer io.Writer
writer *internal.ProtoStreamWriter
lastNameWritten string
}
func newHashCacheWriter(w io.Writer) *hashcacheWriter {
hcw := &hashcacheWriter{
writer: w,
writer: internal.NewProtoStreamWriter(w, internal.ProtoStreamTypeHashCache),
}
io.WriteString(w, "HASHCACHE:v1\n")
return hcw
}
func (hcw *hashcacheWriter) WriteEntry(e hashCacheEntry) error {
func (hcw *hashcacheWriter) WriteEntry(e HashCacheEntry) error {
if hcw.lastNameWritten != "" {
if isLessOrEqual(e.Name, hcw.lastNameWritten) {
return fmt.Errorf("out-of-order directory entry, previous '%v' current '%v'", hcw.lastNameWritten, e.Name)
@@ -109,7 +83,7 @@ func (hcw *hashcacheWriter) WriteEntry(e hashCacheEntry) error {
hcw.lastNameWritten = e.Name
}
fmt.Fprintf(hcw.writer, "%v|%v|%v\n", e.Name, e.Hash, e.ObjectID)
hcw.writer.Write(&e)
return nil
}
+52
View File
@@ -0,0 +1,52 @@
// Code generated by protoc-gen-go.
// source: fs/hashcache.proto
// DO NOT EDIT!
package fs
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import kopia_repo "github.com/kopia/kopia/repo"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type HashCacheEntry struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Hash uint64 `protobuf:"fixed64,2,opt,name=hash" json:"hash,omitempty"`
ObjectId *kopia_repo.ObjectID `protobuf:"bytes,3,opt,name=object_id,json=objectId" json:"object_id,omitempty"`
}
func (m *HashCacheEntry) Reset() { *m = HashCacheEntry{} }
func (m *HashCacheEntry) String() string { return proto.CompactTextString(m) }
func (*HashCacheEntry) ProtoMessage() {}
func (*HashCacheEntry) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} }
func (m *HashCacheEntry) GetObjectId() *kopia_repo.ObjectID {
if m != nil {
return m.ObjectId
}
return nil
}
func init() {
proto.RegisterType((*HashCacheEntry)(nil), "kopia.fs.HashCacheEntry")
}
var fileDescriptor1 = []byte{
// 173 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0x4a, 0x2b, 0xd6, 0xcf,
0x48, 0x2c, 0xce, 0x48, 0x4e, 0x4c, 0xce, 0x48, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2,
0xc8, 0xce, 0x2f, 0xc8, 0x4c, 0xd4, 0x4b, 0x2b, 0x96, 0x12, 0x2e, 0x4a, 0x2d, 0xc8, 0xd7, 0xcf,
0x4f, 0xca, 0x4a, 0x4d, 0x2e, 0xc9, 0x4c, 0x81, 0x48, 0x2b, 0x65, 0x73, 0xf1, 0x79, 0x00, 0x75,
0x38, 0x83, 0x74, 0xb8, 0xe6, 0x95, 0x14, 0x55, 0x0a, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6,
0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x81, 0xd9, 0x20, 0x31, 0x90, 0xb9, 0x12, 0x4c, 0x40,
0x31, 0xb6, 0x20, 0x30, 0x5b, 0xc8, 0x90, 0x8b, 0x13, 0x62, 0x56, 0x7c, 0x66, 0x8a, 0x04, 0x33,
0x50, 0x82, 0xdb, 0x48, 0x44, 0x0f, 0x62, 0x19, 0xc8, 0x22, 0x3d, 0x7f, 0xb0, 0xa4, 0xa7, 0x4b,
0x10, 0x07, 0x44, 0x99, 0x67, 0x8a, 0x93, 0x74, 0x94, 0x64, 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92,
0x5e, 0x72, 0x7e, 0xae, 0x3e, 0x58, 0x2d, 0x94, 0x4c, 0x2b, 0x4e, 0x62, 0x03, 0x3b, 0xc8, 0x18,
0x10, 0x00, 0x00, 0xff, 0xff, 0xa7, 0x08, 0x96, 0x2b, 0xc5, 0x00, 0x00, 0x00,
}
+12
View File
@@ -0,0 +1,12 @@
syntax = "proto3";
package kopia.fs;
option go_package = "github.com/kopia/kopia/fs";
import "repo/objectid.proto";
message HashCacheEntry {
string name = 1;
fixed64 hash = 2;
repo.ObjectID object_id = 3;
}
@@ -35,8 +35,8 @@ func (imd *inmemoryDirectory) addFile(name string, content []byte, mode os.FileM
inmemoryEntry: inmemoryEntry{
parent: imd,
metadata: &EntryMetadata{
Name: name,
FileMode: mode,
Name: name,
ModeAndPermissions: uint32(mode),
},
},
source: bytes.NewBuffer(content),
@@ -54,8 +54,8 @@ func (imd *inmemoryDirectory) addDir(name string, mode os.FileMode) *inmemoryDir
inmemoryEntry: inmemoryEntry{
parent: imd,
metadata: &EntryMetadata{
Name: name,
FileMode: mode | os.ModeDir,
Name: name,
ModeAndPermissions: uint32(mode | os.ModeDir),
},
},
}
@@ -88,7 +88,7 @@ func (imd *inmemoryDirectory) subdir(name ...string) *inmemoryDirectory {
if i2 == nil {
panic(fmt.Sprintf("'%s' not found in '%s'", n, i.metadata.Name))
}
if !i2.Metadata().FileMode.IsDir() {
if !i2.Metadata().FileMode().IsDir() {
panic(fmt.Sprintf("'%s' is not a directory in '%s'", n, i.metadata.Name))
}
+3 -3
View File
@@ -107,9 +107,9 @@ func NewFilesystemDirectory(path string, parent Directory) (Directory, error) {
func entryMetadataFromFileInfo(fi os.FileInfo) *EntryMetadata {
e := &EntryMetadata{
Name: filepath.Base(fi.Name()),
FileMode: fi.Mode(),
ModTime: fi.ModTime(),
Name: filepath.Base(fi.Name()),
ModeAndPermissions: uint32(fi.Mode()),
ModTimestampNano: fi.ModTime().UnixNano(),
}
if fi.Mode().IsRegular() {
@@ -9,8 +9,8 @@ import (
func (e *EntryMetadata) populatePlatformSpecificEntryDetails(fi os.FileInfo) error {
if stat, ok := fi.Sys().(*syscall.Stat_t); ok {
e.OwnerID = stat.Uid
e.GroupID = stat.Gid
e.Uid = stat.Uid
e.Gid = stat.Gid
}
return nil
@@ -64,19 +64,19 @@ func TestFiles(t *testing.T) {
goodCount := 0
if entries[0].Metadata().Name == "f1" && entries[0].Metadata().FileSize == 5 && entries[0].Metadata().FileMode.IsRegular() {
if entries[0].Metadata().Name == "f1" && entries[0].Metadata().FileSize == 5 && entries[0].Metadata().FileMode().IsRegular() {
goodCount++
}
if entries[1].Metadata().Name == "f2" && entries[1].Metadata().FileSize == 4 && entries[1].Metadata().FileMode.IsRegular() {
if entries[1].Metadata().Name == "f2" && entries[1].Metadata().FileSize == 4 && entries[1].Metadata().FileMode().IsRegular() {
goodCount++
}
if entries[2].Metadata().Name == "f3" && entries[2].Metadata().FileSize == 3 && entries[2].Metadata().FileMode.IsRegular() {
if entries[2].Metadata().Name == "f3" && entries[2].Metadata().FileSize == 3 && entries[2].Metadata().FileMode().IsRegular() {
goodCount++
}
if entries[3].Metadata().Name == "y" && entries[3].Metadata().FileSize == 0 && entries[3].Metadata().FileMode.IsDir() {
if entries[3].Metadata().Name == "y" && entries[3].Metadata().FileSize == 0 && entries[3].Metadata().FileMode().IsDir() {
goodCount++
}
if entries[4].Metadata().Name == "z" && entries[4].Metadata().FileSize == 0 && entries[4].Metadata().FileMode.IsDir() {
if entries[4].Metadata().Name == "z" && entries[4].Metadata().FileSize == 0 && entries[4].Metadata().FileMode().IsDir() {
goodCount++
}
if goodCount != 5 {
File renamed without changes.
+7 -7
View File
@@ -18,7 +18,7 @@ type repositoryFile repositoryEntry
type repositorySymlink repositoryEntry
func (rd *repositoryDirectory) Readdir() (Entries, error) {
r, err := rd.repo.Open(rd.entry.Metadata().ObjectID)
r, err := rd.repo.Open(*rd.entry.Metadata().ObjectId)
if err != nil {
return nil, err
}
@@ -38,7 +38,7 @@ func (rd *repositoryDirectory) Readdir() (Entries, error) {
}
func (rf *repositoryFile) Open() (EntryMetadataReadCloser, error) {
r, err := rf.repo.Open(rf.entry.Metadata().ObjectID)
r, err := rf.repo.Open(*rf.entry.Metadata().ObjectId)
if err != nil {
return nil, err
}
@@ -51,7 +51,7 @@ func (rsl *repositorySymlink) Readlink() (string, error) {
}
func newRepoEntry(r repo.Repository, md *EntryMetadata, parent Directory) Entry {
switch md.FileMode & os.ModeType {
switch md.FileMode() & os.ModeType {
case os.ModeDir:
return Directory(&repositoryDirectory{
entry: newEntry(md, parent),
@@ -71,7 +71,7 @@ func newRepoEntry(r repo.Repository, md *EntryMetadata, parent Directory) Entry
})
default:
panic(fmt.Sprintf("not supported entry metadata type: %v", md.FileMode))
panic(fmt.Sprintf("not supported entry metadata type: %v", md.FileMode()))
}
}
@@ -94,9 +94,9 @@ func withMetadata(rc io.ReadCloser, md *EntryMetadata) EntryMetadataReadCloser {
// NewRepositoryDirectory returns Directory based on repository object with the specified ID.
func NewRepositoryDirectory(r repo.Repository, objectID repo.ObjectID) Directory {
d := newRepoEntry(r, &EntryMetadata{
Name: "/",
ObjectID: objectID,
FileMode: 0555 | os.ModeDir,
Name: "/",
ObjectId: &objectID,
ModeAndPermissions: 0555 | uint32(os.ModeDir),
}, nil)
return d.(Directory)
+37 -31
View File
@@ -36,7 +36,7 @@ type UploadResult struct {
// Uploader supports efficient uploading files and directories to repository.
type Uploader interface {
UploadDir(dir Directory, previousManifestID repo.ObjectID) (*UploadResult, error)
UploadDir(dir Directory, previousManifestID *repo.ObjectID) (*UploadResult, error)
UploadFile(file File) (*UploadResult, error)
Cancel()
}
@@ -74,11 +74,13 @@ func (u *uploader) uploadFileInternal(f File, relativePath string, forceStored b
return nil, 0, err
}
e2.ObjectID, err = writer.Result(forceStored)
r, err := writer.Result(forceStored)
if err != nil {
return nil, 0, err
}
e2.ObjectId = &r
if written != e2.FileSize {
// file changed
}
@@ -123,10 +125,11 @@ func (u *uploader) uploadBundleInternal(b *uploadBundle) (*EntryMetadata, uint64
}
b.files = uploadedFiles
bundleMetadata.ObjectID, err = writer.Result(true)
r, err := writer.Result(true)
if err != nil {
return nil, 0, err
}
bundleMetadata.ObjectId = &r
return bundleMetadata, bundleMetadata.metadataHash(), nil
}
@@ -134,16 +137,16 @@ func (u *uploader) uploadBundleInternal(b *uploadBundle) (*EntryMetadata, uint64
func (u *uploader) UploadFile(file File) (*UploadResult, error) {
result := &UploadResult{}
e, _, err := u.uploadFileInternal(file, file.Metadata().Name, true)
result.ObjectID = e.ObjectID
result.ObjectID = *e.ObjectId
return result, err
}
func (u *uploader) UploadDir(dir Directory, previousManifestID repo.ObjectID) (*UploadResult, error) {
func (u *uploader) UploadDir(dir Directory, previousManifestID *repo.ObjectID) (*UploadResult, error) {
var mr hashcacheReader
var err error
if previousManifestID != "" {
if r, err := u.repo.Open(previousManifestID); err == nil {
if previousManifestID != nil {
if r, err := u.repo.Open(*previousManifestID); err == nil {
mr.open(r)
}
}
@@ -178,7 +181,7 @@ func (u *uploader) uploadDirInternal(
entries, err := dir.Readdir()
if err != nil {
return "", 0, false, err
return repo.NullObjectID, 0, false, err
}
entries = u.bundleEntries(entries)
@@ -207,20 +210,22 @@ func (u *uploader) uploadDirInternal(
case Directory:
oid, h, wasCached, err := u.uploadDirInternal(result, entry, entryRelativePath, hcw, mr, false)
if err != nil {
return "", 0, false, err
return repo.NullObjectID, 0, false, err
}
//log.Printf("dirHash: %v %v", fullPath, h)
hash = h
allCached = allCached && wasCached
e.ObjectID = oid
e.ObjectId = &oid
case Symlink:
l, err := entry.Readlink()
if err != nil {
return "", 0, false, err
return repo.NullObjectID, 0, false, err
}
e.ObjectID = repo.NewInlineObjectID([]byte(l))
id := repo.NewInlineObjectID([]byte(l))
e.ObjectId = &id
hash = e.metadataHash()
case *uploadBundle:
@@ -239,13 +244,14 @@ func (u *uploader) uploadDirInternal(
if cacheMatches {
result.Stats.CachedFiles++
// Avoid hashing by reusing previous object ID.
e.ObjectID = repo.ObjectID(cachedEntry.ObjectID)
id := *cachedEntry.ObjectId
e.ObjectId = &id
hash = cachedEntry.Hash
} else {
result.Stats.NonCachedFiles++
e, hash, err = u.uploadBundleInternal(entry)
if err != nil {
return "", 0, false, fmt.Errorf("unable to hash file: %s", err)
return repo.NullObjectID, 0, false, fmt.Errorf("unable to hash file: %s", err)
}
}
@@ -262,18 +268,19 @@ func (u *uploader) uploadDirInternal(
if cacheMatches {
result.Stats.CachedFiles++
// Avoid hashing by reusing previous object ID.
e.ObjectID = repo.ObjectID(cachedEntry.ObjectID)
id := *cachedEntry.ObjectId
e.ObjectId = &id
hash = cachedEntry.Hash
} else {
result.Stats.NonCachedFiles++
e, hash, err = u.uploadFileInternal(entry, entryRelativePath, false)
if err != nil {
return "", 0, false, fmt.Errorf("unable to hash file: %s", err)
return repo.NullObjectID, 0, false, fmt.Errorf("unable to hash file: %s", err)
}
}
default:
return "", 0, false, fmt.Errorf("file type %v not supported", entry.Metadata().FileMode)
return repo.NullObjectID, 0, false, fmt.Errorf("file type %v not supported", entry.Metadata().FileMode())
}
if hash != 0 {
@@ -283,22 +290,20 @@ func (u *uploader) uploadDirInternal(
}
if err := dw.WriteEntry(e, childrenMetadata); err != nil {
return "", 0, false, err
return repo.NullObjectID, 0, false, err
}
if !e.FileMode.IsDir() && e.ObjectID.Type().IsStored() {
if err := hcw.WriteEntry(hashCacheEntry{
if !e.FileMode().IsDir() && e.ObjectId.StorageBlock != "" {
if err := hcw.WriteEntry(HashCacheEntry{
Name: entryRelativePath,
Hash: e.metadataHash(),
ObjectID: e.ObjectID,
Hash: hash,
ObjectId: e.ObjectId,
}); err != nil {
return "", 0, false, err
return repo.NullObjectID, 0, false, err
}
}
}
dw.Close()
var directoryOID repo.ObjectID
dirHash := dirHasher.Sum64()
@@ -308,7 +313,7 @@ func (u *uploader) uploadDirInternal(
if allCached {
// Avoid hashing directory listing if every entry matched the cache.
result.Stats.CachedDirectories++
directoryOID = repo.ObjectID(cachedDirEntry.ObjectID)
directoryOID = *cachedDirEntry.ObjectId
} else {
result.Stats.NonCachedDirectories++
directoryOID, err = writer.Result(forceStored)
@@ -317,14 +322,15 @@ func (u *uploader) uploadDirInternal(
}
}
if err := hcw.WriteEntry(hashCacheEntry{
if err := hcw.WriteEntry(HashCacheEntry{
Name: relativePath + "/",
ObjectID: directoryOID,
ObjectId: &directoryOID,
Hash: dirHash,
}); err != nil {
return "", 0, false, err
return repo.NullObjectID, 0, false, err
}
// log.Printf("Dir: %v %v %v %v", relativePath, directoryOID.UIString(), dirHash, allCached)
return directoryOID, dirHash, allCached, nil
}
@@ -398,8 +404,8 @@ func (u *uploader) bundleEntries(entries Entries) Entries {
func getBundleNumber(md *EntryMetadata) int {
// TODO(jkowalski): This is not ready yet, uncomment when ready.
// if md.FileMode.IsRegular() && md.FileSize < maxBundleFileSize {
// return md.ModTime.Year()*100 + int(md.ModTime.Month())
// if md.FileMode().IsRegular() && md.FileSize < maxBundleFileSize {
// return md.ModTime().Year()*100 + int(md.ModTime().Month())
// }
return 0
+19 -19
View File
@@ -41,8 +41,8 @@ func newUploadTestHarness() *uploadTestHarness {
}
format := repo.Format{
Version: "1",
ObjectFormat: "md5",
Version: 1,
ObjectFormat: repo.ObjectIDFormat_TESTONLY_MD5,
MaxBlobSize: 1000,
MaxInlineBlobSize: 0,
}
@@ -86,22 +86,22 @@ func TestUpload(t *testing.T) {
th := newUploadTestHarness()
defer th.cleanup()
r1, err := th.uploader.UploadDir(th.sourceDir, "")
r1, err := th.uploader.UploadDir(th.sourceDir, nil)
if err != nil {
t.Errorf("upload failed: %v", err)
}
r2, err := th.uploader.UploadDir(th.sourceDir, r1.ManifestID)
r2, err := th.uploader.UploadDir(th.sourceDir, &r1.ManifestID)
if err != nil {
t.Errorf("upload failed: %v", err)
}
if r2.ObjectID != r1.ObjectID {
t.Errorf("expected r1.ObjectID==r2.ObjectID, got %v and %v", r1.ObjectID, r2.ObjectID)
if !r2.ObjectID.Equals(r1.ObjectID) {
t.Errorf("expected r1.ObjectID==r2.ObjectID, got %v and %v", r1.ObjectID.UIString(), r2.ObjectID.UIString())
}
if r2.ManifestID != r1.ManifestID {
t.Errorf("expected r2.ManifestID==r1.ManifestID, got %v and %v", r2.ManifestID, r1.ManifestID)
if !r2.ManifestID.Equals(r1.ManifestID) {
t.Errorf("expected r2.ManifestID==r1.ManifestID, got %v and %v", r2.ManifestID.UIString(), r1.ManifestID.UIString())
}
if r1.Stats.CachedFiles+r1.Stats.CachedDirectories != 0 {
@@ -116,17 +116,17 @@ func TestUpload(t *testing.T) {
// Add one more file, the r1.ObjectID should change.
th.sourceDir.addFile("d2/d1/f3", []byte{1, 2, 3, 4, 5}, 0777)
r3, err := th.uploader.UploadDir(th.sourceDir, r1.ManifestID)
r3, err := th.uploader.UploadDir(th.sourceDir, &r1.ManifestID)
if err != nil {
t.Errorf("upload failed: %v", err)
}
if r2.ObjectID == r3.ObjectID {
t.Errorf("expected r3.ObjectID!=r2.ObjectID, got %v", r3.ObjectID)
if r2.ObjectID.Equals(r3.ObjectID) {
t.Errorf("expected r3.ObjectID!=r2.ObjectID, got %v", r3.ObjectID.UIString())
}
if r2.ManifestID == r3.ManifestID {
t.Errorf("expected r3.ManifestID!=r2.ManifestID, got %v", r3.ManifestID)
if r2.ManifestID.Equals(r3.ManifestID) {
t.Errorf("expected r3.ManifestID!=r2.ManifestID, got %v", r3.ManifestID.UIString())
}
if r3.Stats.NonCachedFiles != 1 && r3.Stats.NonCachedDirectories != 3 {
@@ -137,15 +137,15 @@ func TestUpload(t *testing.T) {
// Now remove the added file, OID should be identical to the original before the file got added.
th.sourceDir.subdir("d2", "d1").remove("f3")
r4, err := th.uploader.UploadDir(th.sourceDir, r1.ManifestID)
r4, err := th.uploader.UploadDir(th.sourceDir, &r1.ManifestID)
if err != nil {
t.Errorf("upload failed: %v", err)
}
if r4.ObjectID != r1.ObjectID {
if !r4.ObjectID.Equals(r1.ObjectID) {
t.Errorf("expected r4.ObjectID==r1.ObjectID, got %v and %v", r4.ObjectID, r1.ObjectID)
}
if r4.ManifestID != r1.ManifestID {
if !r4.ManifestID.Equals(r1.ManifestID) {
t.Errorf("expected r4.ManifestID==r1.ManifestID, got %v and %v", r4.ManifestID, r1.ManifestID)
}
@@ -156,7 +156,7 @@ func TestUpload(t *testing.T) {
}
// Upload again, this time using r3.ManifestID as base.
r5, err := th.uploader.UploadDir(th.sourceDir, r3.ManifestID)
r5, err := th.uploader.UploadDir(th.sourceDir, &r3.ManifestID)
if err != nil {
t.Errorf("upload failed: %v", err)
}
@@ -176,7 +176,7 @@ func TestUpload_TopLevelDirectoryReadFailure(t *testing.T) {
th.sourceDir.failReaddir(errTest)
r, err := th.uploader.UploadDir(th.sourceDir, "")
r, err := th.uploader.UploadDir(th.sourceDir, nil)
if err != errTest {
t.Errorf("expected error: %v", err)
}
@@ -192,7 +192,7 @@ func TestUpload_SubDirectoryReadFailure(t *testing.T) {
th.sourceDir.subdir("d1").failReaddir(errTest)
_, err := th.uploader.UploadDir(th.sourceDir, "")
_, err := th.uploader.UploadDir(th.sourceDir, nil)
if err == nil {
t.Errorf("expected error")
}
+7 -7
View File
@@ -21,11 +21,11 @@ type fuseNode struct {
func (n *fuseNode) Attr(ctx context.Context, a *fuse.Attr) error {
m := n.entry.Metadata()
a.Mode = m.FileMode
a.Mode = m.FileMode()
a.Size = uint64(m.FileSize)
a.Mtime = m.ModTime
a.Uid = m.OwnerID
a.Gid = m.GroupID
a.Mtime = m.ModTime()
a.Uid = m.Uid
a.Gid = m.Gid
return nil
}
@@ -88,7 +88,7 @@ func (dir *fuseDirectoryNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, er
Name: m.Name,
}
switch m.FileMode & os.ModeType {
switch m.FileMode() & os.ModeType {
case os.ModeDir:
dirent.Type = fuse.DT_Dir
case 0:
@@ -112,7 +112,7 @@ type fuseSymlinkNode struct {
}
func (sl *fuseSymlinkNode) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) {
return string(sl.entry.Metadata().ObjectID), nil
return sl.entry.(fs.Symlink).Readlink()
}
func newFuseNode(e fs.Entry) (fusefs.Node, error) {
@@ -124,7 +124,7 @@ func newFuseNode(e fs.Entry) (fusefs.Node, error) {
case fs.Symlink:
return &fuseSymlinkNode{fuseNode{e}}, nil
default:
return nil, fmt.Errorf("entry type not supported: %v", e.Metadata().FileMode)
return nil, fmt.Errorf("entry type not supported: %v", e.Metadata().FileMode())
}
}
+66
View File
@@ -0,0 +1,66 @@
package internal
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"github.com/golang/protobuf/proto"
)
// ProtoStreamReader reads a stream of length-prefix proto messages with an optional header.
type ProtoStreamReader struct {
reader *bufio.Reader
readBuf []byte
buf *proto.Buffer
header []byte
}
// Read reads the next proto message from the stream, returns io.EOF on the end of stream.
func (r *ProtoStreamReader) Read(v proto.Message) error {
v.Reset()
if r.header != nil {
p := make([]byte, len(r.header))
if _, err := r.reader.Read(p); err == io.EOF {
return io.EOF
}
if !bytes.Equal(p, r.header) {
return fmt.Errorf("invalid stream header: %v, expected %v", string(p), string(r.header))
}
r.header = nil
}
length64, err := binary.ReadUvarint(r.reader)
if err != nil {
return err
}
length := int(length64)
if cap(r.readBuf) < length {
r.readBuf = make([]byte, length*12/10)
}
r.readBuf = r.readBuf[:length]
if _, err := io.ReadFull(r.reader, r.readBuf); err != nil {
return err
}
r.buf.SetBuf(r.readBuf)
return r.buf.Unmarshal(v)
}
// NewProtoStreamReader returns new ProtoStreamReader on top of a given buffered reader.
// The provided header must match the beginning of a stream.
func NewProtoStreamReader(r *bufio.Reader, header []byte) *ProtoStreamReader {
return &ProtoStreamReader{
reader: r,
header: header,
buf: proto.NewBuffer(nil),
}
}
+74
View File
@@ -0,0 +1,74 @@
package internal
import (
"bufio"
"bytes"
"io"
"strings"
"testing"
)
var testHeader1 = []byte("01234567")
var testHeader2 = []byte("0123456x")
func TestProtoStream(t *testing.T) {
var buf bytes.Buffer
data := []TestProto{
TestProto{Name: "foo"},
TestProto{Name: "bar"},
TestProto{Name: "baz"},
}
w := NewProtoStreamWriter(&buf, testHeader1)
for _, d := range data {
if err := w.Write(&d); err != nil {
t.Errorf("write error: %v", err)
}
}
r := NewProtoStreamReader(bufio.NewReader(&buf), testHeader1)
var v TestProto
for _, d := range data {
if err := r.Read(&v); err != nil {
t.Errorf("read error: %v", err)
}
if v.Name != d.Name {
t.Errorf("invalid value: '%v', expected '%v'", v.Name, d.Name)
}
}
err := r.Read(&v)
if err != io.EOF {
t.Errorf("expected EOF, got %v", err)
}
}
func TestEmptyStream(t *testing.T) {
var buf bytes.Buffer
NewProtoStreamWriter(&buf, testHeader1)
r := NewProtoStreamReader(bufio.NewReader(&buf), testHeader2)
var v TestProto
err := r.Read(&v)
if err != io.EOF {
t.Errorf("expected EOF, got %v", err)
}
}
func TestInvalidHeader(t *testing.T) {
var buf bytes.Buffer
w := NewProtoStreamWriter(&buf, testHeader1)
if err := w.Write(&TestProto{Name: "foo"}); err != nil {
t.Errorf("write error: %v", err)
}
r := NewProtoStreamReader(bufio.NewReader(&buf), testHeader2)
var v TestProto
err := r.Read(&v)
if err == nil {
t.Errorf("expected error, got none")
} else if !strings.Contains(err.Error(), "invalid stream header") {
t.Errorf("got incorrect error: %v", err)
}
}
+12
View File
@@ -0,0 +1,12 @@
package internal
var (
// ProtoStreamTypeHashCache indicates proto stream whose elements are hash cache entries.
ProtoStreamTypeHashCache = []byte("kopiaHCH")
// ProtoStreamTypeDir indicates proto stream whose elements are directory entries.
ProtoStreamTypeDir = []byte("kopiaDIR")
// ProtoStreamTypeIndirect indicates proto stream whose elements are indirect block references.
ProtoStreamTypeIndirect = []byte("kopiaIND")
)
+58
View File
@@ -0,0 +1,58 @@
package internal
import (
"encoding/binary"
"io"
"github.com/golang/protobuf/proto"
)
// ProtoStreamWriter writes a stream of length-prefix proto messages with an optional header.
type ProtoStreamWriter struct {
writer io.Writer
buf *proto.Buffer
lengthPrefixBuf []byte
header []byte
}
// Write emits length-prefixed proto message to its output.
func (w *ProtoStreamWriter) Write(v proto.Message) error {
if w.header != nil {
if _, err := w.writer.Write(w.header); err != nil {
return err
}
w.header = nil
}
w.buf.Reset()
if err := w.buf.Marshal(v); err != nil {
return err
}
protoBytes := w.buf.Bytes()
if w.lengthPrefixBuf == nil {
w.lengthPrefixBuf = make([]byte, binary.MaxVarintLen64)
}
lengthBytes := w.lengthPrefixBuf[0:binary.PutUvarint(w.lengthPrefixBuf[:], uint64(len(protoBytes)))]
if _, err := w.writer.Write(lengthBytes); err != nil {
return err
}
if _, err := w.writer.Write(protoBytes); err != nil {
return err
}
return nil
}
// NewProtoStreamWriter creates a new ProtoStreamWriter on top of a specified writer with a specified optional header.
func NewProtoStreamWriter(w io.Writer, header []byte) *ProtoStreamWriter {
return &ProtoStreamWriter{
writer: w,
buf: proto.NewBuffer(nil),
header: header,
}
}
+51
View File
@@ -0,0 +1,51 @@
// Code generated by protoc-gen-go.
// source: internal/proto_test.proto
// DO NOT EDIT!
/*
Package internal is a generated protocol buffer package.
It is generated from these files:
internal/proto_test.proto
It has these top-level messages:
TestProto
*/
package internal
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
const _ = proto.ProtoPackageIsVersion1
type TestProto struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
func (m *TestProto) Reset() { *m = TestProto{} }
func (m *TestProto) String() string { return proto.CompactTextString(m) }
func (*TestProto) ProtoMessage() {}
func (*TestProto) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func init() {
proto.RegisterType((*TestProto)(nil), "kopia.fs.TestProto")
}
var fileDescriptor0 = []byte{
// 109 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0xcc, 0x2b, 0x49,
0x2d, 0xca, 0x4b, 0xcc, 0xd1, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x8f, 0x2f, 0x49, 0x2d, 0x2e, 0xd1,
0x03, 0x33, 0x85, 0x38, 0xb2, 0xf3, 0x0b, 0x32, 0x13, 0xf5, 0xd2, 0x8a, 0x95, 0xe4, 0xb9, 0x38,
0x43, 0x80, 0xe2, 0x01, 0x60, 0x61, 0x21, 0x2e, 0x96, 0xbc, 0xc4, 0xdc, 0x54, 0x09, 0x46, 0x05,
0x46, 0x0d, 0xce, 0x20, 0x30, 0xdb, 0x49, 0x31, 0x4a, 0x3e, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49,
0x2f, 0x39, 0x3f, 0x57, 0x1f, 0xac, 0x0f, 0x4a, 0xc2, 0x8c, 0x4f, 0x62, 0x03, 0x1b, 0x6a, 0x0c,
0x08, 0x00, 0x00, 0xff, 0xff, 0x49, 0x42, 0xd9, 0xa7, 0x71, 0x00, 0x00, 0x00,
}
+8
View File
@@ -0,0 +1,8 @@
syntax = "proto3";
package kopia.fs;
option go_package = "github.com/kopia/kopia/internal";
message TestProto {
string name = 1;
}
+71 -84
View File
@@ -5,9 +5,10 @@ import (
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"errors"
"fmt"
"hash"
)
@@ -16,36 +17,83 @@ var (
hkdfInfoCryptoKey = []byte("CryptoKey")
)
// Format describes the format of object data.
type Format struct {
Version string `json:"version"`
ObjectFormat string `json:"objectFormat"`
Secret []byte `json:"secret,omitempty"`
MaxInlineBlobSize int `json:"maxInlineBlobSize"`
MaxBlobSize int `json:"maxBlobSize"`
// ObjectIDFormatInfo performs hashing and encryption of a data block.
type ObjectIDFormatInfo interface {
Name() string
HashBuffer(data []byte, secret []byte) (blockID []byte, cryptoKey []byte)
CreateCipher(key []byte) (cipher.Block, error)
}
// ObjectIDFormat describes single format ObjectID
type ObjectIDFormat struct {
Name string
IsEncrypted bool
hashBuffer func(data []byte, secret []byte) ([]byte, []byte)
createCipher func(key []byte) (cipher.Block, error)
type unencryptedFormat struct {
name string
hashCtor func() hash.Hash
fold int
}
// ObjectIDFormats is a collection of ObjectIDFormat
type ObjectIDFormats []*ObjectIDFormat
func (fi *unencryptedFormat) Name() string {
return fi.name
}
// Find returns the ObjectIDFormat with a given name or nil if not found.
func (fmts ObjectIDFormats) Find(name string) *ObjectIDFormat {
for _, f := range fmts {
if f.Name == name {
return f
func (fi *unencryptedFormat) HashBuffer(data []byte, secret []byte) (blockID []byte, cryptoKey []byte) {
blockID = hashContent(fi.hashCtor, data, secret)
if fi.fold > 0 {
for i := fi.fold; i < len(blockID); i++ {
blockID[i%fi.fold] ^= blockID[i]
}
blockID = blockID[0:fi.fold]
}
return
}
func (fi *unencryptedFormat) CreateCipher(key []byte) (cipher.Block, error) {
return nil, errors.New("encryption not supported")
}
type encryptedFormat struct {
name string
hashCtor func() hash.Hash
createCipher func(key []byte) (cipher.Block, error)
keyBytes int
}
func (fi *encryptedFormat) Name() string {
return fi.name
}
func (fi *encryptedFormat) HashBuffer(data []byte, secret []byte) (blockID []byte, cryptoKey []byte) {
h := hashContent(fi.hashCtor, data, secret)
p := len(h) - fi.keyBytes
blockID = h[0:p]
cryptoKey = h[p:]
return
}
func (fi *encryptedFormat) CreateCipher(key []byte) (cipher.Block, error) {
return fi.createCipher(key)
}
// SupportedFormats containes supported ObjectIDformats
var SupportedFormats = map[ObjectIDFormat]ObjectIDFormatInfo{
ObjectIDFormat_TESTONLY_MD5: &unencryptedFormat{"TESTONLY_MD5", md5.New, 0},
ObjectIDFormat_UNENCYPTED_HMAC_SHA256: &unencryptedFormat{"UNENCRYPTED_HMAC_SHA256", sha256.New, 0},
ObjectIDFormat_UNENCYPTED_HMAC_SHA256_128: &unencryptedFormat{"UNENCRYPTED_HMAC_SHA256", sha256.New, 16},
ObjectIDFormat_ENCRYPTED_HMAC_SHA512_384_AES256: &encryptedFormat{"ENCRYPTED_HMAC_SHA512_384_AES256", sha512.New384, aes.NewCipher, 32},
ObjectIDFormat_ENCRYPTED_HMAC_SHA512_AES256: &encryptedFormat{"ENCRYPTED_HMAC_SHA512_AES256", sha512.New, aes.NewCipher, 32},
}
// DefaultObjectFormat is the format that should be used by default when creating new repositories.
var DefaultObjectFormat = SupportedFormats[ObjectIDFormat_ENCRYPTED_HMAC_SHA512_AES256]
// ParseObjectIDFormat parses the given string and returns corresponding ObjectIDFormat.
func ParseObjectIDFormat(s string) (ObjectIDFormat, error) {
v, ok := ObjectIDFormat_value[s]
if ok {
return ObjectIDFormat(v), nil
}
return nil
return ObjectIDFormat_INVALID, fmt.Errorf("unknown object ID format: %v", s)
}
func fold(b []byte, size int) []byte {
@@ -59,15 +107,6 @@ func fold(b []byte, size int) []byte {
return b[0:size]
}
func nonEncryptedFormat(name string, hf func() hash.Hash, hashSize int) *ObjectIDFormat {
return &ObjectIDFormat{
Name: name,
hashBuffer: func(data []byte, secret []byte) ([]byte, []byte) {
return fold(hashContent(hf, data, secret), hashSize), nil
},
}
}
func hashContent(hf func() hash.Hash, data []byte, secret []byte) []byte {
var h hash.Hash
@@ -79,55 +118,3 @@ func hashContent(hf func() hash.Hash, data []byte, secret []byte) []byte {
h.Write(data)
return h.Sum(nil)
}
func encryptedFormat(
name string,
hf func() hash.Hash,
blockIDSize int,
createCipher func(key []byte) (cipher.Block, error),
keySize int,
) *ObjectIDFormat {
return &ObjectIDFormat{
Name: name,
hashBuffer: func(data []byte, secret []byte) ([]byte, []byte) {
contentHash := hashContent(sha512.New, data, secret)
blockID := fold(hashContent(hf, hkdfInfoBlockID, contentHash), blockIDSize)
cryptoKey := fold(hashContent(hf, hkdfInfoCryptoKey, contentHash), keySize)
return blockID, cryptoKey
},
createCipher: createCipher,
}
}
func buildObjectIDFormats() ObjectIDFormats {
var result ObjectIDFormats
for _, h := range []struct {
name string
hash func() hash.Hash
hashSize int
}{
{"md5", md5.New, md5.Size},
{"sha1", sha1.New, sha1.Size},
{"sha224", sha256.New224, sha256.Size224},
{"sha256", sha256.New, sha256.Size},
{"sha256-fold128", sha256.New, 16},
{"sha256-fold160", sha256.New, 20},
{"sha384", sha512.New384, sha512.Size384},
{"sha512-fold128", sha512.New, 16},
{"sha512-fold160", sha512.New, 20},
{"sha512-224", sha512.New512_224, sha512.Size224},
{"sha512-256", sha512.New512_256, sha512.Size256},
{"sha512", sha512.New, sha512.Size},
} {
result = append(result, nonEncryptedFormat(h.name, h.hash, h.hashSize))
result = append(result, encryptedFormat(h.name+"-aes128", h.hash, h.hashSize, aes.NewCipher, 16))
result = append(result, encryptedFormat(h.name+"-aes192", h.hash, h.hashSize, aes.NewCipher, 24))
result = append(result, encryptedFormat(h.name+"-aes256", h.hash, h.hashSize, aes.NewCipher, 32))
}
return result
}
// SupportedFormats contains supported repository formats.
var SupportedFormats = buildObjectIDFormats()
+141
View File
@@ -0,0 +1,141 @@
// Code generated by protoc-gen-go.
// source: repo/format.proto
// DO NOT EDIT!
/*
Package repo is a generated protocol buffer package.
It is generated from these files:
repo/format.proto
repo/indirect.proto
repo/objectid.proto
It has these top-level messages:
Format
IndirectObjectEntry
ObjectID
*/
package repo
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
const _ = proto.ProtoPackageIsVersion1
type EncryptionAlgorithm int32
const (
EncryptionAlgorithm_NONE EncryptionAlgorithm = 0
EncryptionAlgorithm_AES EncryptionAlgorithm = 1
)
var EncryptionAlgorithm_name = map[int32]string{
0: "NONE",
1: "AES",
}
var EncryptionAlgorithm_value = map[string]int32{
"NONE": 0,
"AES": 1,
}
func (x EncryptionAlgorithm) String() string {
return proto.EnumName(EncryptionAlgorithm_name, int32(x))
}
func (EncryptionAlgorithm) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
// ObjectIDFormat describes the format of repository objects.
type ObjectIDFormat int32
const (
// Format not specified.
ObjectIDFormat_INVALID ObjectIDFormat = 0
// MD5 (no encryption, MD5 hashing, only used for testing)
ObjectIDFormat_TESTONLY_MD5 ObjectIDFormat = 1
// HMAC-SHA256 hash of content used as object ID, no encryption.
ObjectIDFormat_UNENCYPTED_HMAC_SHA256 ObjectIDFormat = 2
// HMAC-SHA256 hash folded into 128 bits used as object ID, no encryption.
ObjectIDFormat_UNENCYPTED_HMAC_SHA256_128 ObjectIDFormat = 3
// HMAC-SHA512 hash of content, top part of the hash used as 256-bit object ID,
// bottom part used as 256-bit encryption key.
ObjectIDFormat_ENCRYPTED_HMAC_SHA512_AES256 ObjectIDFormat = 4
// HMAC-SHA512_384 hash over contents, top part of the hash used as 128-bit object ID,
// bottom part used as 256-bit encryption key.
ObjectIDFormat_ENCRYPTED_HMAC_SHA512_384_AES256 ObjectIDFormat = 5
)
var ObjectIDFormat_name = map[int32]string{
0: "INVALID",
1: "TESTONLY_MD5",
2: "UNENCYPTED_HMAC_SHA256",
3: "UNENCYPTED_HMAC_SHA256_128",
4: "ENCRYPTED_HMAC_SHA512_AES256",
5: "ENCRYPTED_HMAC_SHA512_384_AES256",
}
var ObjectIDFormat_value = map[string]int32{
"INVALID": 0,
"TESTONLY_MD5": 1,
"UNENCYPTED_HMAC_SHA256": 2,
"UNENCYPTED_HMAC_SHA256_128": 3,
"ENCRYPTED_HMAC_SHA512_AES256": 4,
"ENCRYPTED_HMAC_SHA512_384_AES256": 5,
}
func (x ObjectIDFormat) String() string {
return proto.EnumName(ObjectIDFormat_name, int32(x))
}
func (ObjectIDFormat) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
// Format describes the format of object data.
type Format struct {
Version int32 `protobuf:"varint,1,opt,name=version" json:"version,omitempty"`
ObjectFormat ObjectIDFormat `protobuf:"varint,2,opt,name=object_format,json=objectFormat,enum=kopia.repo.ObjectIDFormat" json:"object_format,omitempty"`
Secret []byte `protobuf:"bytes,3,opt,name=secret,proto3" json:"secret,omitempty"`
MaxInlineBlobSize int32 `protobuf:"varint,4,opt,name=max_inline_blob_size,json=maxInlineBlobSize" json:"max_inline_blob_size,omitempty"`
MaxBlobSize int32 `protobuf:"varint,5,opt,name=max_blob_size,json=maxBlobSize" json:"max_blob_size,omitempty"`
}
func (m *Format) Reset() { *m = Format{} }
func (m *Format) String() string { return proto.CompactTextString(m) }
func (*Format) ProtoMessage() {}
func (*Format) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func init() {
proto.RegisterType((*Format)(nil), "kopia.repo.Format")
proto.RegisterEnum("kopia.repo.EncryptionAlgorithm", EncryptionAlgorithm_name, EncryptionAlgorithm_value)
proto.RegisterEnum("kopia.repo.ObjectIDFormat", ObjectIDFormat_name, ObjectIDFormat_value)
}
var fileDescriptor0 = []byte{
// 357 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x91, 0xcb, 0x6a, 0xf2, 0x40,
0x14, 0xc7, 0xbf, 0x78, 0xfd, 0x38, 0x5e, 0x88, 0xd3, 0x22, 0xc1, 0x5e, 0x10, 0xe9, 0x42, 0x5c,
0x44, 0x8c, 0xb5, 0xb8, 0x2b, 0xd1, 0xa4, 0x18, 0xd0, 0x58, 0x12, 0x5b, 0xb0, 0x9b, 0xc1, 0x84,
0xd4, 0xa6, 0x35, 0x99, 0x30, 0xa6, 0xa5, 0xed, 0x6b, 0xf5, 0x45, 0xfa, 0x48, 0x9d, 0x8c, 0x8a,
0x08, 0x6e, 0x06, 0xce, 0xfc, 0x7e, 0xe7, 0x3f, 0x67, 0x38, 0x50, 0xa1, 0x5e, 0x44, 0xda, 0xcf,
0x84, 0x06, 0x8b, 0x58, 0x8e, 0x28, 0x89, 0x09, 0x82, 0x37, 0x12, 0xf9, 0x0b, 0x39, 0x01, 0x8d,
0x5f, 0x01, 0x72, 0x77, 0x1c, 0x22, 0x09, 0xf2, 0x1f, 0x1e, 0x5d, 0xfb, 0x24, 0x94, 0x84, 0xba,
0xd0, 0xcc, 0x5a, 0xbb, 0x12, 0xdd, 0x42, 0x89, 0x38, 0xaf, 0x9e, 0x1b, 0xe3, 0x4d, 0x8e, 0x94,
0x62, 0xbc, 0xac, 0xd4, 0xe4, 0x7d, 0x90, 0x3c, 0xe5, 0x82, 0xa1, 0x6d, 0xc2, 0xac, 0xe2, 0xa6,
0x61, 0x1b, 0x5d, 0x85, 0xdc, 0xda, 0x73, 0xa9, 0x17, 0x4b, 0x69, 0xd6, 0x59, 0xb4, 0xb6, 0x15,
0x6a, 0xc3, 0x69, 0xb0, 0xf8, 0xc4, 0x7e, 0xb8, 0xf2, 0x43, 0x0f, 0x3b, 0x2b, 0xe2, 0xe0, 0xb5,
0xff, 0xed, 0x49, 0x19, 0xfe, 0x7e, 0x85, 0x31, 0x83, 0xa3, 0x01, 0x23, 0x36, 0x03, 0xa8, 0x01,
0xa5, 0xa4, 0x61, 0x6f, 0x66, 0xb9, 0x59, 0x60, 0x97, 0x3b, 0xa7, 0xd5, 0x84, 0x13, 0x3d, 0x74,
0xe9, 0x57, 0x14, 0xb3, 0xd9, 0xd5, 0xd5, 0x92, 0x50, 0x3f, 0x7e, 0x09, 0xd0, 0x7f, 0xc8, 0x98,
0x53, 0x53, 0x17, 0xff, 0xa1, 0x3c, 0xa4, 0x55, 0xdd, 0x16, 0x85, 0xd6, 0x8f, 0x00, 0xe5, 0xc3,
0xb9, 0x51, 0x01, 0xf2, 0x86, 0xf9, 0xa8, 0x8e, 0x0d, 0x8d, 0x89, 0x22, 0x14, 0x67, 0xba, 0x3d,
0x9b, 0x9a, 0xe3, 0x39, 0x9e, 0x68, 0x3d, 0x51, 0x40, 0x35, 0xa8, 0x3e, 0x98, 0xba, 0x39, 0x9c,
0xdf, 0xcf, 0x74, 0x0d, 0x8f, 0x26, 0xea, 0x10, 0xdb, 0x23, 0x55, 0xe9, 0xdd, 0x88, 0x29, 0x74,
0x09, 0xb5, 0xe3, 0x0c, 0x77, 0x94, 0xbe, 0x98, 0x46, 0x75, 0x38, 0x67, 0xd4, 0x3a, 0xc4, 0xbd,
0x8e, 0x82, 0xd9, 0x30, 0x49, 0x42, 0x06, 0x5d, 0x41, 0xfd, 0xb8, 0xd1, 0xed, 0x5f, 0xef, 0xac,
0xec, 0xe0, 0xe2, 0xe9, 0x6c, 0xc9, 0x7e, 0xf4, 0xee, 0xc8, 0x2e, 0x09, 0xda, 0x7c, 0x05, 0xdb,
0x33, 0x59, 0x84, 0x93, 0xe3, 0x4b, 0xee, 0xfe, 0x05, 0x00, 0x00, 0xff, 0xff, 0x3b, 0xcf, 0x39,
0xd2, 0xf9, 0x01, 0x00, 0x00,
}
+41
View File
@@ -0,0 +1,41 @@
syntax = "proto3";
package kopia.repo;
option go_package = "github.com/kopia/kopia/repo";
enum EncryptionAlgorithm {
NONE = 0;
AES = 1;
}
// ObjectIDFormat describes the format of repository objects.
enum ObjectIDFormat {
// Format not specified.
INVALID = 0;
// MD5 (no encryption, MD5 hashing, only used for testing)
TESTONLY_MD5 = 1;
// HMAC-SHA256 hash of content used as object ID, no encryption.
UNENCYPTED_HMAC_SHA256 = 2;
// HMAC-SHA256 hash folded into 128 bits used as object ID, no encryption.
UNENCYPTED_HMAC_SHA256_128 = 3;
// HMAC-SHA512 hash of content, top part of the hash used as 256-bit object ID,
// bottom part used as 256-bit encryption key.
ENCRYPTED_HMAC_SHA512_AES256 = 4;
// HMAC-SHA512_384 hash over contents, top part of the hash used as 128-bit object ID,
// bottom part used as 256-bit encryption key.
ENCRYPTED_HMAC_SHA512_384_AES256 = 5;
}
// Format describes the format of object data.
message Format {
int32 version = 1;
ObjectIDFormat object_format = 2;
bytes secret = 3;
int32 max_inline_blob_size = 4;
int32 max_blob_size = 5;
}
+52
View File
@@ -0,0 +1,52 @@
// Code generated by protoc-gen-go.
// source: repo/indirect.proto
// DO NOT EDIT!
package repo
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// IndirectObjectEntry represents an entry in indirect object proto stream.
type IndirectObjectEntry struct {
Start int64 `protobuf:"varint,1,opt,name=start" json:"start,omitempty"`
Length int64 `protobuf:"varint,2,opt,name=length" json:"length,omitempty"`
Object *ObjectID `protobuf:"bytes,3,opt,name=object" json:"object,omitempty"`
}
func (m *IndirectObjectEntry) Reset() { *m = IndirectObjectEntry{} }
func (m *IndirectObjectEntry) String() string { return proto.CompactTextString(m) }
func (*IndirectObjectEntry) ProtoMessage() {}
func (*IndirectObjectEntry) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} }
func (m *IndirectObjectEntry) GetObject() *ObjectID {
if m != nil {
return m.Object
}
return nil
}
func init() {
proto.RegisterType((*IndirectObjectEntry)(nil), "kopia.repo.IndirectObjectEntry")
}
var fileDescriptor1 = []byte{
// 165 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0x2e, 0x4a, 0x2d, 0xc8,
0xd7, 0xcf, 0xcc, 0x4b, 0xc9, 0x2c, 0x4a, 0x4d, 0x2e, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17,
0xe2, 0xca, 0xce, 0x2f, 0xc8, 0x4c, 0xd4, 0x03, 0x49, 0x49, 0x41, 0x14, 0xe4, 0x27, 0x65, 0x01,
0xa5, 0x33, 0x53, 0x20, 0x0a, 0x94, 0x0a, 0xb9, 0x84, 0x3d, 0xa1, 0x5a, 0xfc, 0xc1, 0x32, 0xae,
0x79, 0x25, 0x45, 0x95, 0x42, 0x22, 0x5c, 0xac, 0xc5, 0x25, 0x89, 0x45, 0x25, 0x12, 0x8c, 0x0a,
0x8c, 0x1a, 0xcc, 0x41, 0x10, 0x8e, 0x90, 0x18, 0x17, 0x5b, 0x4e, 0x6a, 0x5e, 0x7a, 0x49, 0x86,
0x04, 0x13, 0x58, 0x18, 0xca, 0x13, 0xd2, 0xe1, 0x62, 0x83, 0x18, 0x2b, 0xc1, 0x0c, 0x14, 0xe7,
0x36, 0x12, 0xd1, 0x43, 0x58, 0xab, 0x07, 0x31, 0xd6, 0xd3, 0x25, 0x08, 0xaa, 0xc6, 0x49, 0x36,
0x4a, 0x3a, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0x1f, 0xac, 0x12, 0x4a,
0x82, 0xd4, 0x27, 0xb1, 0x81, 0x1d, 0x66, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x0c, 0xb4, 0xa1,
0x12, 0xd0, 0x00, 0x00, 0x00,
}
+13
View File
@@ -0,0 +1,13 @@
syntax = "proto3";
package kopia.repo;
option go_package = "github.com/kopia/kopia/repo";
import "repo/objectid.proto";
// IndirectObjectEntry represents an entry in indirect object proto stream.
message IndirectObjectEntry {
int64 start = 1;
int64 length = 2;
repo.ObjectID object = 3;
}
+15 -25
View File
@@ -3,32 +3,16 @@ package repo
import (
"fmt"
"io"
"github.com/kopia/kopia/storage"
)
type seekTableEntry struct {
startOffset int64
length int64
blockID string
}
func (r *seekTableEntry) endOffset() int64 {
return r.startOffset + int64(r.length)
}
func (r *seekTableEntry) String() string {
return fmt.Sprintf("start: %d len: %d end: %d block: %s",
r.startOffset,
r.length,
r.endOffset(),
r.blockID)
func (i *IndirectObjectEntry) endOffset() int64 {
return i.Start + i.Length
}
type objectReader struct {
storage storage.Storage
repo Repository
seekTable []seekTableEntry
seekTable []IndirectObjectEntry
currentPosition int64 // Overall position in the objectReader
totalLength int64 // Overall length
@@ -80,13 +64,19 @@ func (r *objectReader) Read(buffer []byte) (int, error) {
}
func (r *objectReader) openCurrentChunk() error {
blockID := r.seekTable[r.currentChunkIndex].blockID
blockData, err := r.storage.GetBlock(blockID)
st := r.seekTable[r.currentChunkIndex]
blockData, err := r.repo.Open(*st.Object)
if err != nil {
return err
}
defer blockData.Close()
r.currentChunkData = blockData
b := make([]byte, st.Length)
if _, err := io.ReadFull(blockData, b); err != nil {
return err
}
r.currentChunkData = b
r.currentChunkPosition = 0
return nil
}
@@ -101,7 +91,7 @@ func (r *objectReader) findChunkIndexForOffset(offset int64) int {
for left <= right {
middle := (left + right) / 2
if offset < r.seekTable[middle].startOffset {
if offset < r.seekTable[middle].Start {
right = middle - 1
continue
}
@@ -136,7 +126,7 @@ func (r *objectReader) Seek(offset int64, whence int) (int64, error) {
index := r.findChunkIndexForOffset(offset)
chunkStartOffset := r.seekTable[index].startOffset
chunkStartOffset := r.seekTable[index].Start
if index != r.currentChunkIndex {
r.closeCurrentChunk()
+60 -35
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"github.com/kopia/kopia/internal"
"github.com/kopia/kopia/storage"
)
@@ -44,15 +45,15 @@ type objectWriter struct {
prefix string
listWriter *objectWriter
listProtoWriter *internal.ProtoStreamWriter
listCurrentPos int64
flushedObjectCount int
lastFlushedObject ObjectID
description string
objectType ObjectIDType
description string
indirectLevel int32
blockTracker *blockTracker
atomicWrites bool
}
func (w *objectWriter) Close() error {
@@ -68,9 +69,13 @@ func (w *objectWriter) Close() error {
return nil
}
func (w *objectWriter) Write(data []byte) (n int, err error) {
dataLen := len(data)
remaining := len(data)
func (w *objectWriter) WriteGather(dataSlices [][]byte) (n int, err error) {
dataLen := 0
for _, s := range dataSlices {
dataLen += len(s)
}
remaining := dataLen
w.totalLength += int64(remaining)
for remaining > 0 {
@@ -80,33 +85,45 @@ func (w *objectWriter) Write(data []byte) (n int, err error) {
room := w.buffer.Cap() - w.buffer.Len()
if remaining <= room {
w.buffer.Write(data)
for _, s := range dataSlices {
w.buffer.Write(s)
}
dataSlices = nil
remaining = 0
} else {
if w.atomicWrites {
if w.buffer == nil {
// We're at the beginning of a buffer, fail if the buffer is too small.
return 0, fmt.Errorf("object writer buffer too small, need: %v, have: %v", remaining, room)
}
if err := w.flushBuffer(false); err != nil {
return 0, err
}
for room > 0 {
var chunk []byte
continue
if len(dataSlices[0]) >= room {
chunk = dataSlices[0][0:room]
dataSlices[0] = dataSlices[0][len(chunk):]
} else {
chunk = dataSlices[0]
dataSlices[0] = nil
}
w.buffer.Write(chunk)
remaining -= len(chunk)
room -= len(chunk)
if len(dataSlices[0]) == 0 {
dataSlices = dataSlices[1:]
}
}
w.buffer.Write(data[0:room])
if err := w.flushBuffer(false); err != nil {
return 0, err
}
data = data[room:]
remaining = len(data)
}
}
return dataLen, nil
}
func (w *objectWriter) Write(data []byte) (n int, err error) {
var b [1][]byte
b[0] = data
return w.WriteGather(b[:])
}
func (w *objectWriter) flushBuffer(force bool) error {
// log.Printf("flushing bufer")
// defer log.Printf("flushed")
@@ -118,36 +135,43 @@ func (w *objectWriter) flushBuffer(force bool) error {
b := w.buffer
w.buffer = nil
objectID, blockReader, err := w.repo.hashBufferForWriting(b, string(w.objectType)+w.prefix)
objectID, blockReader, err := w.repo.hashBufferForWriting(b, w.prefix)
if err != nil {
return err
}
if err := w.repo.storage.PutBlock(objectID.BlockID(), blockReader, storage.PutOptionsDefault); err != nil {
if err := w.repo.storage.PutBlock(objectID.StorageBlock, blockReader, storage.PutOptionsDefault); err != nil {
return fmt.Errorf(
"error when flushing chunk %d of %s to %#v: %#v",
w.flushedObjectCount,
w.description,
objectID.BlockID(),
objectID.StorageBlock,
err)
}
w.blockTracker.addBlock(objectID.BlockID())
w.blockTracker.addBlock(objectID.StorageBlock)
w.flushedObjectCount++
w.lastFlushedObject = objectID
if w.listWriter == nil {
w.listWriter = &objectWriter{
repo: w.repo,
objectType: ObjectIDTypeList,
prefix: w.prefix,
description: "LIST(" + w.description + ")",
atomicWrites: true,
blockTracker: w.blockTracker,
repo: w.repo,
indirectLevel: w.indirectLevel + 1,
prefix: w.prefix,
description: "LIST(" + w.description + ")",
blockTracker: w.blockTracker,
}
w.listProtoWriter = internal.NewProtoStreamWriter(w.listWriter, internal.ProtoStreamTypeIndirect)
w.listCurrentPos = 0
}
fmt.Fprintf(w.listWriter, "%v,%v\n", length, objectID)
w.listProtoWriter.Write(&IndirectObjectEntry{
Object: &objectID,
Start: w.listCurrentPos,
Length: int64(length),
})
w.listCurrentPos += int64(length)
}
return nil
}
@@ -155,10 +179,10 @@ func (w *objectWriter) flushBuffer(force bool) error {
func (w *objectWriter) Result(forceStored bool) (ObjectID, error) {
if !forceStored && w.flushedObjectCount == 0 {
if w.buffer == nil {
return "B", nil
return NullObjectID, nil
}
if w.buffer.Len() < w.repo.format.MaxInlineBlobSize {
if w.buffer.Len() < int(w.repo.format.MaxInlineBlobSize) {
return NewInlineObjectID(w.buffer.Bytes()), nil
}
}
@@ -171,9 +195,10 @@ func (w *objectWriter) Result(forceStored bool) (ObjectID, error) {
}()
if w.flushedObjectCount == 1 {
w.lastFlushedObject.Indirect = w.indirectLevel
return w.lastFlushedObject, nil
} else if w.flushedObjectCount == 0 {
return "", nil
return NullObjectID, nil
} else {
return w.listWriter.Result(true)
}
+89 -156
View File
@@ -4,87 +4,72 @@ import (
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"unicode/utf8"
"github.com/golang/protobuf/proto"
"fmt"
)
// NullObjectID is the identifier of an null object.
var NullObjectID ObjectID
const objectIDEncryptionInfoSeparator = "."
var (
binaryEncoding = base64.RawURLEncoding
inlineContentEncoding = base64.RawURLEncoding
)
const (
objectIDEncryptionInfoSeparator = "."
)
// UIString returns the name of the repository object that is suitable for displaying in the UI.
// Note that the object ID name may contain its encryption key, which is sensitive.
func (m *ObjectID) UIString() string {
if m.StorageBlock != "" {
var encryptionSuffix string
// ObjectIDType describes the type of the chunk.
type ObjectIDType string
// ObjectEncryptionInfo represents encryption info associated with ObjectID.
type ObjectEncryptionInfo string
// NoEncryption indicates that the object is not encrypted.
var NoEncryption = ObjectEncryptionInfo("")
const (
// ObjectIDTypeText represents text-only inline object ID
ObjectIDTypeText ObjectIDType = "T"
// ObjectIDTypeBinary represents binary inline object ID
ObjectIDTypeBinary ObjectIDType = "B"
// ObjectIDTypeStored represents ID of object whose data is stored directly in a single storage block indicated by string.
ObjectIDTypeStored ObjectIDType = "D"
// ObjectIDTypeList represents ID of an object whose data is stored in mutliple storage blocks.
// The value of the ObjectID is the list chunk, which lists object IDs that need to be concatenated
// to form the contents.
ObjectIDTypeList ObjectIDType = "L"
// ObjectIDTypeSection represents ID of an object whose data is a section of another object.
// The format is S{offset},{length},{base}
ObjectIDTypeSection ObjectIDType = "S"
)
// IsStored determines whether data for the given chunk type is stored in the storage
// (as opposed to being stored inline as part of ObjectID itself).
func (ct ObjectIDType) IsStored() bool {
switch ct {
case ObjectIDTypeStored, ObjectIDTypeList:
return true
default:
return false
}
}
// ObjectID represents the identifier of a chunk.
// Identifiers can either refer to data block stored in a Storage, or contain small amounts
// of payload directly (useful for short ASCII or binary files).
type ObjectID string
// Type gets the type of the object ID.
func (c ObjectID) Type() ObjectIDType {
return ObjectIDType(c[0:1])
}
// InlineData returns inline data stored as part of ObjectID. For chunk IDs representing stored
// chunks, the value is nil.
func (c ObjectID) InlineData() []byte {
payload := string(c[1:])
switch c.Type() {
case ObjectIDTypeText:
return []byte(payload)
case ObjectIDTypeBinary:
decodedPayload, err := binaryEncoding.DecodeString(payload)
if err == nil {
return decodedPayload
if m.Encryption != nil {
encryptionSuffix = "." + hex.EncodeToString(m.Encryption)
}
if m.Indirect > 0 {
return fmt.Sprintf("L%v,%v%v", m.Indirect, m.StorageBlock, encryptionSuffix)
}
return "D" + m.StorageBlock + encryptionSuffix
}
return nil
if m.Content != nil {
return "B" + inlineContentEncoding.EncodeToString(m.Content)
}
if m.Section != nil {
return fmt.Sprintf("S%v,%v,%v", m.Section.Start, m.Section.Length, m.Section.Base.UIString())
}
return "B"
}
// Equals determines whether ObjectID is equal to another ObjectID.
func (m *ObjectID) Equals(other ObjectID) bool {
return proto.Equal(m, &other)
}
// NewInlineObjectID returns new ObjectID containing inline binary or text-encoded data.
func NewInlineObjectID(data []byte) ObjectID {
return ObjectID{
Content: data,
}
}
// NewSectionObjectID returns new ObjectID representing a section of an object with a given base ID, start offset and length.
func NewSectionObjectID(start, length int64, baseID *ObjectID) ObjectID {
return ObjectID{
Section: &ObjectID_Section{
Base: baseID,
Start: start,
Length: length,
},
}
}
// parseNumberUntilComma parses a string of the form "{x},{remainder}" where x is a 64-bit number and remainder is arbitrary string.
@@ -104,134 +89,82 @@ func parseNumberUntilComma(s string) (int64, string, error) {
}
func parseSectionInfoString(s string) (int64, int64, ObjectID, error) {
if ObjectIDType(s[0]) != ObjectIDTypeSection {
return 0, -1, "", errors.New("not a section object")
}
var start, length int64
var err error
start, s, err = parseNumberUntilComma(s[1:])
if err != nil {
return 0, -1, "", err
return 0, -1, NullObjectID, err
}
length, s, err = parseNumberUntilComma(s)
if err != nil {
return 0, -1, "", err
return 0, -1, NullObjectID, err
}
oid, err := ParseObjectID(s)
if err != nil {
return 0, -1, "", err
return 0, -1, NullObjectID, err
}
return start, length, oid, nil
}
// SectionInfo returns start, length and the base ID of a section object.
func (c ObjectID) SectionInfo() (start int64, length int64, baseID ObjectID) {
if c.Type() != ObjectIDTypeSection {
return 0, 0, ""
}
start, length, oid, err := parseSectionInfoString(string(c))
if err != nil {
// This should not happen if we came in through ParseObjectID
panic("invalid section info: " + string(c))
}
return start, length, oid
}
// BlockID returns identifier of the storage block. For inline chunk IDs, an empty string is returned.
func (c ObjectID) BlockID() string {
if c.Type().IsStored() {
content := string(c[1:])
firstSeparator := strings.Index(content, objectIDEncryptionInfoSeparator)
if firstSeparator > 0 {
return string(content[0:firstSeparator])
}
return string(content)
}
return ""
}
// EncryptionInfo returns ObjectEncryptionInfo for the ObjectID.
func (c ObjectID) EncryptionInfo() ObjectEncryptionInfo {
if c.Type().IsStored() {
content := string(c[1:])
firstSeparator := strings.Index(content, objectIDEncryptionInfoSeparator)
if firstSeparator > 0 {
return ObjectEncryptionInfo(content[firstSeparator+1:])
}
}
return NoEncryption
}
// NewInlineObjectID returns new ObjectID containing inline binary or text-encoded data.
func NewInlineObjectID(data []byte) ObjectID {
if !utf8.Valid(data) {
return ObjectID("B" + binaryEncoding.EncodeToString(data))
}
for _, b := range data {
if b < 32 && (b != 9 && b != 10 && b != 13) {
// Any other character indicates binary content.
return ObjectID("B" + binaryEncoding.EncodeToString(data))
}
}
return ObjectID("T" + string(data))
}
// NewSectionObjectID returns new ObjectID representing a section of an object with a given base ID, start offset and length.
func NewSectionObjectID(start, length int64, baseID ObjectID) ObjectID {
return ObjectID(fmt.Sprintf("S%v,%v,%v", start, length, baseID))
}
// ParseObjectID converts the specified string into ObjectID.
func ParseObjectID(objectIDString string) (ObjectID, error) {
if len(objectIDString) >= 1 {
chunkType := ObjectIDType(objectIDString[0])
chunkType := objectIDString[0]
content := objectIDString[1:]
switch chunkType {
case ObjectIDTypeSection:
if _, _, _, err := parseSectionInfoString(objectIDString); err == nil {
return ObjectID(objectIDString), nil
case 'S':
if start, length, base, err := parseSectionInfoString(objectIDString); err == nil {
return ObjectID{Section: &ObjectID_Section{
Start: start,
Length: length,
Base: &base,
}}, nil
}
case ObjectIDTypeText:
return ObjectID(objectIDString), nil
case ObjectIDTypeBinary:
if _, err := binaryEncoding.DecodeString(content); err == nil {
return ObjectID(objectIDString), nil
case 'B':
if v, err := inlineContentEncoding.DecodeString(content); err == nil {
return ObjectID{Content: v}, nil
}
case 'I', 'D':
var indirectLevel int32
if chunkType == 'I' {
comma := strings.Index(content, ",")
if comma < 0 {
// malformed
break
}
i, err := strconv.Atoi(content[0:comma])
if err != nil {
break
}
indirectLevel = int32(i)
content = content[comma+1:]
}
case ObjectIDTypeList, ObjectIDTypeStored:
firstSeparator := strings.Index(content, objectIDEncryptionInfoSeparator)
lastSeparator := strings.LastIndex(content, objectIDEncryptionInfoSeparator)
if firstSeparator == lastSeparator {
if firstSeparator == -1 {
// Found zero Separators in the ID - no encryption info.
return ObjectID(objectIDString), nil
return ObjectID{StorageBlock: content, Indirect: indirectLevel}, nil
}
if firstSeparator > 0 {
b, err := hex.DecodeString(content[firstSeparator+1:])
if err == nil && len(b) > 0 && len(b)%2 == 0 {
if err == nil && len(b) > 0 {
// Valid chunk ID with encryption info.
return ObjectID(objectIDString), nil
return ObjectID{StorageBlock: content[0:firstSeparator], Encryption: b}, nil
}
}
}
}
}
return "", fmt.Errorf("malformed chunk id: '%s'", objectIDString)
return NullObjectID, fmt.Errorf("malformed object id: '%s'", objectIDString)
}
+80
View File
@@ -0,0 +1,80 @@
// Code generated by protoc-gen-go.
// source: repo/objectid.proto
// DO NOT EDIT!
package repo
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// ObjectID represents the identifier of a chunk.
// Identifiers can either refer to data block stored in a Storage, or contain small amounts
// of payload directly (useful for short ASCII or binary files).
type ObjectID struct {
StorageBlock string `protobuf:"bytes,1,opt,name=storage_block,json=storageBlock" json:"storage_block,omitempty"`
Indirect int32 `protobuf:"varint,2,opt,name=indirect" json:"indirect,omitempty"`
Encryption []byte `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"`
Content []byte `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
Section *ObjectID_Section `protobuf:"bytes,5,opt,name=section" json:"section,omitempty"`
}
func (m *ObjectID) Reset() { *m = ObjectID{} }
func (m *ObjectID) String() string { return proto.CompactTextString(m) }
func (*ObjectID) ProtoMessage() {}
func (*ObjectID) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} }
func (m *ObjectID) GetSection() *ObjectID_Section {
if m != nil {
return m.Section
}
return nil
}
type ObjectID_Section struct {
Base *ObjectID `protobuf:"bytes,1,opt,name=base" json:"base,omitempty"`
Start int64 `protobuf:"varint,2,opt,name=start" json:"start,omitempty"`
Length int64 `protobuf:"varint,3,opt,name=length" json:"length,omitempty"`
}
func (m *ObjectID_Section) Reset() { *m = ObjectID_Section{} }
func (m *ObjectID_Section) String() string { return proto.CompactTextString(m) }
func (*ObjectID_Section) ProtoMessage() {}
func (*ObjectID_Section) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0, 0} }
func (m *ObjectID_Section) GetBase() *ObjectID {
if m != nil {
return m.Base
}
return nil
}
func init() {
proto.RegisterType((*ObjectID)(nil), "kopia.repo.ObjectID")
proto.RegisterType((*ObjectID_Section)(nil), "kopia.repo.ObjectID.Section")
}
var fileDescriptor2 = []byte{
// 256 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xcf, 0x4a, 0x03, 0x31,
0x10, 0xc6, 0x69, 0xb7, 0xdb, 0xad, 0xd3, 0x7a, 0x89, 0x45, 0x42, 0xfd, 0x83, 0xe8, 0xa5, 0xa7,
0x14, 0x2a, 0xf8, 0x00, 0xc5, 0x8b, 0x27, 0x21, 0xde, 0xbc, 0x48, 0x92, 0x86, 0x6d, 0x6c, 0xcd,
0x2c, 0xd9, 0xf1, 0xe0, 0xdb, 0xf8, 0xa8, 0x66, 0xb3, 0xbb, 0xea, 0xa1, 0x97, 0xc0, 0xf7, 0xfd,
0x66, 0x86, 0x1f, 0x81, 0xb3, 0x60, 0x2b, 0x5c, 0xa1, 0x7e, 0xb7, 0x86, 0xdc, 0x56, 0x54, 0x01,
0x09, 0x19, 0xec, 0xb1, 0x72, 0x4a, 0x34, 0xe8, 0xf6, 0x7b, 0x08, 0x93, 0xe7, 0x84, 0x9f, 0x1e,
0xd9, 0x1d, 0x9c, 0xd6, 0x84, 0x41, 0x95, 0xf6, 0x4d, 0x1f, 0xd0, 0xec, 0xf9, 0xe0, 0x66, 0xb0,
0x3c, 0x91, 0xb3, 0xae, 0xdc, 0x34, 0x1d, 0x5b, 0xc0, 0xc4, 0xf9, 0xad, 0x0b, 0x71, 0x85, 0x0f,
0x23, 0xcf, 0xe5, 0x6f, 0x66, 0xd7, 0x00, 0xd6, 0x9b, 0xf0, 0x55, 0x91, 0x43, 0xcf, 0xb3, 0x48,
0x67, 0xf2, 0x5f, 0xc3, 0x38, 0x14, 0x06, 0x3d, 0x59, 0x4f, 0x7c, 0x94, 0x60, 0x1f, 0xd9, 0x03,
0x14, 0x75, 0xe3, 0x18, 0xd7, 0xf2, 0x48, 0xa6, 0xeb, 0x4b, 0xf1, 0x67, 0x29, 0x7a, 0x43, 0xf1,
0xd2, 0xce, 0xc8, 0x7e, 0x78, 0xa1, 0xa0, 0xe8, 0x3a, 0xb6, 0x84, 0x91, 0x56, 0xb5, 0x4d, 0xd2,
0xd3, 0xf5, 0xfc, 0xd8, 0xbe, 0x4c, 0x13, 0x6c, 0x0e, 0x79, 0x4d, 0x2a, 0xb4, 0xfe, 0x99, 0x6c,
0x03, 0x3b, 0x87, 0xf1, 0xc1, 0xfa, 0x92, 0x76, 0x49, 0x3c, 0x93, 0x5d, 0xda, 0x5c, 0xbd, 0x5e,
0x94, 0x8e, 0x76, 0x9f, 0x5a, 0x18, 0xfc, 0x58, 0xa5, 0xab, 0xdd, 0xdb, 0xdc, 0xd6, 0xe3, 0xf4,
0xa9, 0xf7, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7b, 0x26, 0x25, 0xb7, 0x6b, 0x01, 0x00, 0x00,
}
+21
View File
@@ -0,0 +1,21 @@
syntax = "proto3";
package kopia.repo;
option go_package = "github.com/kopia/kopia/repo";
// ObjectID represents the identifier of a chunk.
// Identifiers can either refer to data block stored in a Storage, or contain small amounts
// of payload directly (useful for short ASCII or binary files).
message ObjectID {
string storage_block = 1;
int32 indirect = 2; // level of indirection (0==block data, 1=list of blocks, 2=list of lists, ...)
bytes encryption = 3;
bytes content = 4;
message Section {
ObjectID base = 1;
int64 start = 2;
int64 length = 3;
}
Section section = 5;
}
+17 -22
View File
@@ -1,6 +1,7 @@
package repo
import (
"bytes"
"strings"
"testing"
)
@@ -17,19 +18,21 @@ func TestParseMalformedObjectID(t *testing.T) {
"Dab.",
"Dab.a",
"Dab.abc",
"Dab.00",
"Dab.011",
"L.",
"L.x",
"L.af",
"Lx.ag",
"Lab.",
"L1",
"L1,",
"L-1,X",
"Xsomething",
}
for _, c := range cases {
v, err := ParseObjectID(c)
if v != "" || err == nil || !strings.HasPrefix(err.Error(), "malformed chunk id") {
if !v.Equals(NullObjectID) || err == nil || !strings.HasPrefix(err.Error(), "malformed object id") {
t.Errorf("unexpected result for %v: v: %v err: %v", c, v, err)
}
}
@@ -38,24 +41,16 @@ func TestParseMalformedObjectID(t *testing.T) {
func TestParseObjectIDEncryptionInfo(t *testing.T) {
cases := []struct {
objectID string
expected ObjectEncryptionInfo
expected []byte
}{
{"B", NoEncryption},
{"BAQIDBA", NoEncryption},
{"T", NoEncryption},
{"T:foo", NoEncryption},
{"Tfoo\nbar", NoEncryption},
{"Tfoo\nbar:baz", NoEncryption},
{"Dabcdef", NoEncryption},
{"Labcdef", NoEncryption},
{
"Dabcdef.00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
ObjectEncryptionInfo("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"),
},
{
"Labcdef.00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
ObjectEncryptionInfo("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"),
},
{"B", nil},
{"BAQIDBA", nil},
{"Dabcdef", nil},
{"I1,abcdef", nil},
{"I2,abcdef", nil},
{"Dabcdef.00112233", []byte{0x00, 0x11, 0x22, 0x33}},
{"I1,abcdef.0011223344", []byte{0x00, 0x11, 0x22, 0x33, 0x44}},
{"I2,abcdef.0011223344", []byte{0x00, 0x11, 0x22, 0x33, 0x44}},
}
for _, c := range cases {
@@ -65,9 +60,9 @@ func TestParseObjectIDEncryptionInfo(t *testing.T) {
continue
}
actual := objectID.EncryptionInfo()
if actual != c.expected {
t.Errorf("invalid encryption info for %v: %v, expected: %v", c.objectID, actual, c.expected)
actual := objectID.Encryption
if !bytes.Equal(actual, c.expected) {
t.Errorf("invalid encryption info for %v: %x, expected: %x", c.objectID, actual, c.expected)
}
}
}
+82 -88
View File
@@ -10,10 +10,13 @@ import (
"fmt"
"hash"
"io"
"strconv"
"io/ioutil"
"log"
"strings"
"sync/atomic"
"github.com/kopia/kopia/internal"
"github.com/kopia/kopia/storage"
"github.com/kopia/kopia/storage/logging"
)
@@ -76,7 +79,7 @@ type repository struct {
stats *RepositoryStats
format Format
idFormat ObjectIDFormat
idFormat ObjectIDFormatInfo
}
func (repo *repository) Close() {
@@ -107,7 +110,6 @@ func (repo *repository) Storage() storage.Storage {
func (repo *repository) NewWriter(options ...WriterOption) ObjectWriter {
result := &objectWriter{
repo: repo,
objectType: ObjectIDTypeStored,
blockTracker: &blockTracker{},
}
@@ -119,24 +121,30 @@ func (repo *repository) NewWriter(options ...WriterOption) ObjectWriter {
}
func (repo *repository) Open(objectID ObjectID) (ObjectReader, error) {
if start, length, baseID := objectID.SectionInfo(); baseID != "" {
baseReader, err := repo.Open(baseID)
// log.Printf("Repository::Open %v", objectID.String())
// defer log.Printf("finished Repository::Open() %v", objectID.String())
if objectID.Section != nil {
if objectID.Section.Base == nil {
return nil, fmt.Errorf("invalid section base object")
}
baseReader, err := repo.Open(*objectID.Section.Base)
if err != nil {
return nil, fmt.Errorf("cannot create base reader: %v", err)
}
return newObjectSectionReader(start, length, baseReader)
return newObjectSectionReader(objectID.Section.Start, objectID.Section.Length, baseReader)
}
r, err := repo.newRawReader(objectID)
if err != nil {
return nil, err
}
if objectID.Indirect > 0 {
r, err := repo.Open(removeIndirection(objectID))
if err != nil {
return nil, err
}
defer r.Close()
if objectID.Type() == ObjectIDTypeList {
seekTable := make([]seekTableEntry, 0, 100)
seekTable, err = repo.flattenListChunk(seekTable, objectID, r)
seekTable, err := repo.flattenListChunk(r)
if err != nil {
return nil, err
}
@@ -144,13 +152,32 @@ func (repo *repository) Open(objectID ObjectID) (ObjectReader, error) {
totalLength := seekTable[len(seekTable)-1].endOffset()
return &objectReader{
storage: repo.storage,
repo: repo,
seekTable: seekTable,
totalLength: totalLength,
}, nil
}
return r, err
return repo.newRawReader(objectID)
}
func dumpObject(repo *repository, oid ObjectID) {
oid.Indirect = 0
log.Printf(" dumping %v", oid.String())
defer log.Printf(" end of %v", oid.String())
r, err := repo.Open(oid)
if err != nil {
log.Printf("failed to open %v: %v", oid, err)
return
}
defer r.Close()
b, err := ioutil.ReadAll(r)
if err != nil {
log.Printf("failed to read %v: %v", oid, err)
return
}
log.Printf("%x", b)
}
// RepositoryOption controls the behavior of Repository.
@@ -185,14 +212,11 @@ func NewRepository(
f *Format,
options ...RepositoryOption,
) (Repository, error) {
if f.Version != "1" {
return nil, fmt.Errorf("unsupported storage version: %v", f.Version)
}
if f.MaxBlobSize < 100 {
return nil, fmt.Errorf("MaxBlobSize is not set")
}
sf := SupportedFormats.Find(f.ObjectFormat)
sf := SupportedFormats[f.ObjectFormat]
if sf == nil {
return nil, fmt.Errorf("unknown object format: %v", f.ObjectFormat)
}
@@ -203,7 +227,7 @@ func NewRepository(
stats: &RepositoryStats{},
}
repo.idFormat = *sf
repo.idFormat = sf
for _, o := range options {
if err := o(repo); err != nil {
@@ -212,13 +236,13 @@ func NewRepository(
}
}
repo.bufferManager = newBufferManager(repo.format.MaxBlobSize)
repo.bufferManager = newBufferManager(int(repo.format.MaxBlobSize))
return repo, nil
}
func (repo *repository) hashBuffer(data []byte) ([]byte, []byte) {
return repo.idFormat.hashBuffer(data, repo.format.Secret)
return repo.idFormat.HashBuffer(data, repo.format.Secret)
}
func (repo *repository) hashBufferForWriting(buffer *bytes.Buffer, prefix string) (ObjectID, storage.ReaderWithLength, error) {
@@ -232,17 +256,18 @@ func (repo *repository) hashBufferForWriting(buffer *bytes.Buffer, prefix string
contentHash, cryptoKey := repo.hashBuffer(data)
if cryptoKey != nil {
var err error
blockCipher, err = repo.idFormat.createCipher(cryptoKey)
blockCipher, err = repo.idFormat.CreateCipher(cryptoKey)
if err != nil {
return "", nil, err
return NullObjectID, nil, err
}
}
var objectID ObjectID
objectID := ObjectID{
StorageBlock: prefix + hex.EncodeToString(contentHash),
}
if len(cryptoKey) > 0 {
objectID = ObjectID(prefix + hex.EncodeToString(contentHash) + objectIDEncryptionInfoSeparator + hex.EncodeToString(cryptoKey))
} else {
objectID = ObjectID(prefix + hex.EncodeToString(contentHash))
objectID.Encryption = cryptoKey
}
atomic.AddInt32(&repo.stats.HashedBlocks, 1)
@@ -268,71 +293,45 @@ func (repo *repository) hashBufferForWriting(buffer *bytes.Buffer, prefix string
return objectID, blockReader, nil
}
func (repo *repository) flattenListChunk(
seekTable []seekTableEntry,
listObjectID ObjectID,
rawReader io.Reader) ([]seekTableEntry, error) {
func (repo *repository) flattenListChunk(rawReader io.Reader) ([]IndirectObjectEntry, error) {
scanner := bufio.NewScanner(rawReader)
r := bufio.NewReader(rawReader)
pr := internal.NewProtoStreamReader(r, internal.ProtoStreamTypeIndirect)
var seekTable []IndirectObjectEntry
for scanner.Scan() {
c := scanner.Text()
comma := strings.Index(c, ",")
if comma <= 0 {
return nil, fmt.Errorf("unsupported entry '%v' in list '%s'", c, listObjectID)
for {
var oe IndirectObjectEntry
err := pr.Read(&oe)
if err == io.EOF {
break
}
length, err := strconv.ParseInt(c[0:comma], 10, 64)
objectID, err := ParseObjectID(c[comma+1:])
if err != nil {
return nil, fmt.Errorf("unsupported entry '%v' in list '%s': %#v", c, listObjectID, err)
log.Printf("Failed to read proto: %v", err)
return nil, err
}
switch objectID.Type() {
case ObjectIDTypeList:
subreader, err := repo.newRawReader(objectID)
if err != nil {
return nil, err
}
seekTable, err = repo.flattenListChunk(seekTable, objectID, subreader)
if err != nil {
return nil, err
}
case ObjectIDTypeStored:
var startOffset int64
if len(seekTable) > 0 {
startOffset = seekTable[len(seekTable)-1].endOffset()
} else {
startOffset = 0
}
seekTable = append(
seekTable,
seekTableEntry{
blockID: objectID.BlockID(),
startOffset: startOffset,
length: length,
})
default:
return nil, fmt.Errorf("unsupported entry '%v' in list '%v'", objectID, listObjectID)
}
seekTable = append(seekTable, oe)
}
return seekTable, nil
}
func removeIndirection(o ObjectID) ObjectID {
if o.Indirect <= 0 {
panic("removeIndirection() called on a direct object")
}
o.Indirect--
return o
}
func (repo *repository) newRawReader(objectID ObjectID) (ObjectReader, error) {
inline := objectID.InlineData()
if inline != nil {
return newObjectReaderWithData(inline), nil
if objectID.Content != nil {
return newObjectReaderWithData(objectID.Content), nil
}
blockID := objectID.BlockID()
blockID := objectID.StorageBlock
payload, err := repo.storage.GetBlock(blockID)
if err != nil {
return nil, err
@@ -341,19 +340,14 @@ func (repo *repository) newRawReader(objectID ObjectID) (ObjectReader, error) {
atomic.AddInt32(&repo.stats.BlocksReadFromStorage, 1)
atomic.AddInt64(&repo.stats.BytesReadFromStorage, int64(len(payload)))
if objectID.EncryptionInfo() == NoEncryption {
if err := repo.verifyChecksum(payload, objectID.BlockID()); err != nil {
if objectID.Encryption == nil {
if err := repo.verifyChecksum(payload, objectID.StorageBlock); err != nil {
return nil, err
}
return newObjectReaderWithData(payload), nil
}
cryptoKey, err := hex.DecodeString(string(objectID.EncryptionInfo()))
if err != nil {
return nil, errors.New("malformed encryption key")
}
blockCipher, err := repo.idFormat.createCipher(cryptoKey)
blockCipher, err := repo.idFormat.CreateCipher(objectID.Encryption)
if err != nil {
return nil, errors.New("cannot create cipher")
}
@@ -366,7 +360,7 @@ func (repo *repository) newRawReader(objectID ObjectID) (ObjectReader, error) {
// after decrypting the content. This serves as a checksum.
atomic.AddInt64(&repo.stats.DecryptedBytes, int64(len(payload)))
if err := repo.verifyChecksum(payload, objectID.BlockID()); err != nil {
if err := repo.verifyChecksum(payload, objectID.StorageBlock); err != nil {
return nil, err
}
+128 -192
View File
@@ -1,28 +1,28 @@
package repo
import (
"bufio"
"bytes"
"crypto/md5"
cryptorand "crypto/rand"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"math/rand"
"reflect"
"sort"
"strings"
"testing"
"github.com/kopia/kopia/internal"
"github.com/kopia/kopia/storage"
"github.com/kopia/kopia/storage/storagetesting"
)
func testFormat() *Format {
return &Format{
Version: "1",
Version: 1,
MaxBlobSize: 200,
MaxInlineBlobSize: 20,
ObjectFormat: "md5",
ObjectFormat: ObjectIDFormat_TESTONLY_MD5,
}
}
@@ -56,16 +56,15 @@ func TestWriters(t *testing.T) {
data []byte
objectID ObjectID
}{
{[]byte{}, "B"},
{[]byte("quick brown fox"), "Tquick brown fox"},
{[]byte{1, 2, 3, 4}, "BAQIDBA"},
{[]byte{10, 13, 9}, "T\n\r\t"},
{[]byte{0xc2, 0x28}, "Bwig"}, // invalid UTF-8, will be represented as binary
{[]byte{}, NullObjectID},
{[]byte("quick brown fox"), ObjectID{Content: []byte("quick brown fox")}},
{[]byte{1, 2, 3, 4}, ObjectID{Content: []byte{1, 2, 3, 4}}},
{[]byte{0xc2, 0x28}, ObjectID{Content: []byte{0xc2, 0x28}}},
{
[]byte("the quick brown fox jumps over the lazy dog"),
"DX77add1d5f41223d5582fca736a5cb335",
ObjectID{StorageBlock: "X77add1d5f41223d5582fca736a5cb335"},
},
{make([]byte, 100), "DX6d0bb00954ceb7fbee436bb55a8397a9"}, // 100 zero bytes
{make([]byte, 100), ObjectID{StorageBlock: "X6d0bb00954ceb7fbee436bb55a8397a9"}}, // 100 zero bytes
}
for _, c := range cases {
@@ -83,11 +82,11 @@ func TestWriters(t *testing.T) {
continue
}
if result != c.objectID {
t.Errorf("incorrect result for %v, expected: %v got: %v", c.data, c.objectID, result)
if !result.Equals(c.objectID) {
t.Errorf("incorrect result for %v, expected: %v got: %v", c.data, c.objectID.UIString(), result)
}
if !c.objectID.Type().IsStored() {
if c.objectID.StorageBlock == "" {
if len(data) != 0 {
t.Errorf("unexpected data written to the storage: %v", data)
}
@@ -110,120 +109,74 @@ func TestWriterCompleteChunkInTwoWrites(t *testing.T) {
writer.Write(bytes[0:50])
writer.Write(bytes[0:50])
result, err := writer.Result(false)
if string(result) != "DX6d0bb00954ceb7fbee436bb55a8397a9" {
if !result.Equals(ObjectID{StorageBlock: "X6d0bb00954ceb7fbee436bb55a8397a9"}) {
t.Errorf("unexpected result: %v err: %v", result, err)
}
}
func TestWriterListChunk(t *testing.T) {
data, repo := setupTest(t)
func verifyIndirectBlock(t *testing.T, r Repository, oid ObjectID) {
for level := int32(0); level < oid.Indirect; level++ {
direct := oid
direct.Indirect = level
contentBytes := make([]byte, 250)
contentMd5Sum200 := getMd5Digest(contentBytes[0:200]) // hash of 200 zero bytes
contentMd5Sum50 := getMd5Digest(contentBytes[200:250]) // hash of 50 zero bytes
listChunkContent := []byte("200,D" + contentMd5Sum200 + "\n50,D" + contentMd5Sum50 + "\n")
listChunkObjectID := getMd5LObjectID(listChunkContent)
rd, err := r.Open(direct)
if err != nil {
t.Errorf("unable to open %v: %v", oid.String(), err)
return
}
defer rd.Close()
writer := repo.NewWriter()
writer.Write(contentBytes)
result, err := writer.Result(false)
if err != nil {
t.Errorf("error getting writer results: %v", err)
pr := internal.NewProtoStreamReader(bufio.NewReader(rd), internal.ProtoStreamTypeIndirect)
for {
var v IndirectObjectEntry
if err := pr.Read(&v); err != nil {
if err == io.EOF {
break
}
t.Errorf("err: %v", err)
break
}
}
}
if string(result) != listChunkObjectID {
t.Errorf("incorrect list chunk ID: %v, expected: %v", result, listChunkObjectID)
}
// We have 3 chunks - 200 zero bytes, 50 zero bytes and the list.
verifyStorageBlocks(t, writer.StorageBlocks(), data, map[string][]byte{
contentMd5Sum200: contentBytes[0:200],
contentMd5Sum50: contentBytes[0:50],
getMd5Digest(listChunkContent): listChunkContent,
})
}
func TestWriterListOfListsChunk(t *testing.T) {
data, repo := setupTest(t)
contentBytes := make([]byte, 1400)
chunk1Id := getMd5CObjectID(contentBytes[0:200]) // hash of 200 zero bytes
list1ChunkContent := []byte(strings.Repeat("200,"+chunk1Id+"\n", 5))
list1ObjectID := fmt.Sprintf("%v,%v", len(list1ChunkContent), getMd5LObjectID(list1ChunkContent))
list2ChunkContent := []byte(strings.Repeat("200,"+chunk1Id+"\n", 2))
list2ObjectID := fmt.Sprintf("%v,%v", len(list2ChunkContent), getMd5LObjectID(list2ChunkContent))
listOfListsChunkContent := []byte(list1ObjectID + "\n" + list2ObjectID + "\n")
listOfListsObjectID := getMd5LObjectID(listOfListsChunkContent)
writer := repo.NewWriter()
writer.Write(contentBytes)
result, err := writer.Result(false)
if string(result) != listOfListsObjectID || err != nil {
t.Errorf("unexpected result: %v expected: %v, err: %v", result, listOfListsObjectID, err)
func TestIndirection(t *testing.T) {
cases := []struct {
dataLength int
expectedBlockCount int
expectedIndirection int32
}{
{dataLength: 200, expectedBlockCount: 1, expectedIndirection: 0},
{dataLength: 250, expectedBlockCount: 3, expectedIndirection: 1},
{dataLength: 1400, expectedBlockCount: 4, expectedIndirection: 2},
{dataLength: 2000, expectedBlockCount: 5, expectedIndirection: 2},
{dataLength: 3000, expectedBlockCount: 6, expectedIndirection: 2},
{dataLength: 4000, expectedBlockCount: 9, expectedIndirection: 3},
{dataLength: 10000, expectedBlockCount: 16, expectedIndirection: 3},
}
// We have 4 chunks - 200 zero bytes, 2 lists, and 1 list-of-lists.
verifyStorageBlocks(t, writer.StorageBlocks(), data, map[string][]byte{
getMd5Digest(contentBytes[0:200]): contentBytes[0:200],
getMd5Digest(list1ChunkContent): list1ChunkContent,
getMd5Digest(list2ChunkContent): list2ChunkContent,
getMd5Digest(listOfListsChunkContent): listOfListsChunkContent,
})
}
for _, c := range cases {
data, repo := setupTest(t)
func TestWriterListOfListsOfListsChunk(t *testing.T) {
data, repo := setupTest(t)
contentBytes := make([]byte, c.dataLength)
writtenData := make([]byte, 10000)
chunk1Id := getMd5CObjectID(writtenData[0:200]) // hash of 200 zero bytes
writer := repo.NewWriter()
writer.Write(contentBytes)
result, err := writer.Result(false)
if err != nil {
t.Errorf("error getting writer results: %v", err)
}
// First level list chunk has 5 C[] chunk IDs, because that's how many IDs fit in one chunk.
// (that number 200 was chosen for a reason, to make testing easy)
//
// 200,Cfbaf48ec981a5eecdb57b929fdd426e8\n
// 200,Cfbaf48ec981a5eecdb57b929fdd426e8\n
// 200,Cfbaf48ec981a5eecdb57b929fdd426e8\n
// 200,Cfbaf48ec981a5eecdb57b929fdd426e8\n
// 200,Cfbaf48ec981a5eecdb57b929fdd426e8\n
list1ChunkContent := []byte(strings.Repeat("200,"+chunk1Id+"\n", 5))
list1ObjectID := fmt.Sprintf("%v,%v", len(list1ChunkContent), getMd5LObjectID(list1ChunkContent))
if result.Indirect != c.expectedIndirection {
t.Errorf("incorrect indirection level: %v, expected %v", result.Indirect, c.expectedIndirection)
}
// Second level lists L[] chunks from the first level. They have all the same content
// because all lists are identical.
// 190,L52760f658059fef754f5deabdd01df93\n
// 190,L52760f658059fef754f5deabdd01df93\n
// 190,L52760f658059fef754f5deabdd01df93\n
// 190,L52760f658059fef754f5deabdd01df93\n
// 190,L52760f658059fef754f5deabdd01df93\n
list2ChunkContent := []byte(strings.Repeat(string(list1ObjectID)+"\n", 5))
list2ObjectID := fmt.Sprintf("%v,%v", len(list2ChunkContent), getMd5LObjectID(list2ChunkContent))
if len(data) != c.expectedBlockCount {
t.Errorf("unexpected block count: %v, expected %v", len(data), c.expectedBlockCount)
}
// Now those lists are also identical and represent 5000 bytes each, so
// the top-level list-of-lists-of-lists will have 2 entries:
//
// 190,Lb99b28e34c87e4934b4cc5631bb38ee8\n
// 190,Lb99b28e34c87e4934b4cc5631bb38ee8\n
list3ChunkContent := []byte(strings.Repeat(string(list2ObjectID)+"\n", 2))
list3ObjectID := getMd5LObjectID(list3ChunkContent)
writer := repo.NewWriter()
writer.Write(writtenData)
result, err := writer.Result(false)
if string(result) != list3ObjectID {
t.Errorf("unexpected result: %v err: %v", result, err)
verifyIndirectBlock(t, repo, result)
}
// We have 4 data blocks representing 10000 bytes of zero. Not bad!
verifyStorageBlocks(t, writer.StorageBlocks(), data, map[string][]byte{
getMd5Digest(writtenData[0:200]): writtenData[0:200],
getMd5Digest(list1ChunkContent): list1ChunkContent,
getMd5Digest(list2ChunkContent): list2ChunkContent,
getMd5Digest(list3ChunkContent): list3ChunkContent,
})
}
func TestHMAC(t *testing.T) {
@@ -231,7 +184,7 @@ func TestHMAC(t *testing.T) {
content := bytes.Repeat([]byte{0xcd}, 50)
s := testFormat()
s.ObjectFormat = "md5"
s.ObjectFormat = ObjectIDFormat_TESTONLY_MD5
s.Secret = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}
repo, err := NewRepository(storagetesting.NewMapStorage(data), s)
@@ -241,7 +194,7 @@ func TestHMAC(t *testing.T) {
w := repo.NewWriter()
w.Write(content)
result, err := w.Result(false)
if string(result) != "D697eaf0aca3a3aea3a75164746ffaa79" {
if result.UIString() != "D697eaf0aca3a3aea3a75164746ffaa79" {
t.Errorf("unexpected result: %v err: %v", result, err)
}
}
@@ -258,8 +211,6 @@ func TestReader(t *testing.T) {
}{
{"B", []byte{}},
{"BAQIDBA", []byte{1, 2, 3, 4}},
{"T", []byte{}},
{"Tfoo\nbar", []byte("foo\nbar")},
{"Da76999788386641a3ec798554f1fe7e6", storedPayload},
{"S0,2,BAQIDBA", []byte{1, 2}},
{"S1,3,BAQIDBA", []byte{2, 3, 4}},
@@ -353,12 +304,12 @@ func TestEndToEndReadAndSeek(t *testing.T) {
verify(t, repo, objectID, randomData, fmt.Sprintf("%v %v/%v", objectID, forceStored, size))
if size > 1 {
sectionID := NewSectionObjectID(0, int64(size/2), objectID)
sectionID := NewSectionObjectID(0, int64(size/2), &objectID)
verify(t, repo, sectionID, randomData[0:10], fmt.Sprintf("%v %v/%v", sectionID, forceStored, size))
}
if size > 1 {
sectionID := NewSectionObjectID(int64(1), int64(size-1), objectID)
sectionID := NewSectionObjectID(int64(1), int64(size-1), &objectID)
verify(t, repo, sectionID, randomData[1:], fmt.Sprintf("%v %v/%v", sectionID, forceStored, size))
}
}
@@ -397,9 +348,9 @@ func verify(t *testing.T, repo Repository, objectID ObjectID, expectedData []byt
}
func TestFormats(t *testing.T) {
makeFormat := func(objectFormat string) *Format {
makeFormat := func(objectFormat ObjectIDFormat) *Format {
return &Format{
Version: "1",
Version: 1,
ObjectFormat: objectFormat,
Secret: []byte("key"),
MaxBlobSize: 10000,
@@ -411,88 +362,78 @@ func TestFormats(t *testing.T) {
oids map[string]ObjectID
}{
{
format: makeFormat("md5"), // MD5-HMAC with secret 'key'
format: makeFormat(ObjectIDFormat_TESTONLY_MD5), // MD5-HMAC with secret 'key'
oids: map[string]ObjectID{
"": "D63530468a04e386459855da0063b6596",
"The quick brown fox jumps over the lazy dog": "D80070713463e7749b90c2dc24911e275",
"": ObjectID{StorageBlock: "63530468a04e386459855da0063b6596"},
"The quick brown fox jumps over the lazy dog": ObjectID{StorageBlock: "80070713463e7749b90c2dc24911e275"},
},
},
{
format: &Format{
Version: "1",
ObjectFormat: "md5",
Version: 1,
ObjectFormat: ObjectIDFormat_TESTONLY_MD5,
MaxBlobSize: 10000,
Secret: []byte{}, // HMAC with zero-byte secret
},
oids: map[string]ObjectID{
"": "D74e6f7298a9c2d168935f58c001bad88",
"The quick brown fox jumps over the lazy dog": "Dad262969c53bc16032f160081c4a07a0",
"": ObjectID{StorageBlock: "74e6f7298a9c2d168935f58c001bad88"},
"The quick brown fox jumps over the lazy dog": ObjectID{StorageBlock: "ad262969c53bc16032f160081c4a07a0"},
},
},
{
format: &Format{
Version: "1",
ObjectFormat: "md5",
Version: 1,
ObjectFormat: ObjectIDFormat_TESTONLY_MD5,
MaxBlobSize: 10000,
Secret: nil, // non-HMAC version
},
oids: map[string]ObjectID{
"": "Dd41d8cd98f00b204e9800998ecf8427e",
"The quick brown fox jumps over the lazy dog": "D9e107d9d372bb6826bd81d3542a419d6",
"": ObjectID{StorageBlock: "d41d8cd98f00b204e9800998ecf8427e"},
"The quick brown fox jumps over the lazy dog": ObjectID{
StorageBlock: "9e107d9d372bb6826bd81d3542a419d6",
},
},
},
{
format: makeFormat("md5"),
format: makeFormat(ObjectIDFormat_TESTONLY_MD5),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "D80070713463e7749b90c2dc24911e275",
"The quick brown fox jumps over the lazy dog": ObjectID{
StorageBlock: "80070713463e7749b90c2dc24911e275",
},
},
},
{
format: makeFormat("sha1"),
format: makeFormat(ObjectIDFormat_UNENCYPTED_HMAC_SHA256),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Dde7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9",
"The quick brown fox jumps over the lazy dog": ObjectID{
StorageBlock: "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8",
},
},
},
{
format: makeFormat("sha256"),
format: makeFormat(ObjectIDFormat_UNENCYPTED_HMAC_SHA256_128),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Df7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8",
"The quick brown fox jumps over the lazy dog": ObjectID{
StorageBlock: "18f1da557915937d2675055a87758d9b",
},
},
},
{
format: makeFormat("sha512"),
format: makeFormat(ObjectIDFormat_ENCRYPTED_HMAC_SHA512_384_AES256),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Db42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a",
"The quick brown fox jumps over the lazy dog": ObjectID{
StorageBlock: "d7f4727e2c0b39ae0f1e40cc96f60242",
Encryption: mustParseBase16("d5b7801841cea6fc592c5d3e1ae50700582a96cf35e1e554995fe4e03381c237"),
},
},
},
{
format: makeFormat("sha256-fold128-aes128"),
format: makeFormat(ObjectIDFormat_ENCRYPTED_HMAC_SHA512_AES256),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "D028e74c630aee6400db4f84d49d1ed08.2825273c3344999e26081d99bcaf3f18",
},
},
{
format: makeFormat("sha256-aes128"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "D26bb3fee9e83a7f6ff8397f33460cd0d24354b28ae2d41b6f2376fbe7db12005.2825273c3344999e26081d99bcaf3f18",
},
},
{
format: makeFormat("sha384-aes256"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "D08edca658caee5ae86c9f1e4ccea26b1a43b0ddb04502c5f1b1cba051a756a9eba440bb6991b23323e37556ec5cf4f88.7a753377319650870f5cba47c30b608675542f206fa67d94dcbb9a0ccce7f467",
},
},
{
format: makeFormat("sha512-aes256"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Deee04ad71248ef4ffec5159558e80e2791e32299cfb79a1e063cc5f4a71cd61ca2c1b110e3ae2e83635a8626a3a27e4805eb745c40b5a4ebd4c9372602e5ab65.1c0b1b58ce05b7b8b05cfce27a485ddf97bde5159f6946357ec7795236f36a84",
},
},
{
format: makeFormat("sha512-fold128-aes192"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Dd829ad027ee4ff394f6a61615eb30d16.0ab00e0e0e156927f0be63372e8449d7bf2316c43d46e626",
"The quick brown fox jumps over the lazy dog": ObjectID{
StorageBlock: "b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb",
Encryption: mustParseBase16("82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a"),
},
},
},
}
@@ -501,7 +442,7 @@ func TestFormats(t *testing.T) {
data := map[string][]byte{}
st := storagetesting.NewMapStorage(data)
t.Logf("verifying %#v", c.format)
t.Logf("verifying %v", c.format.String())
repo, err := NewRepository(st, c.format)
if err != nil {
t.Errorf("cannot create manager: %v", err)
@@ -516,8 +457,8 @@ func TestFormats(t *testing.T) {
if err != nil {
t.Errorf("error: %v", err)
}
if oid != v {
t.Errorf("invalid oid for #%v %v/%v: %v expected %v", caseIndex, c.format.ObjectFormat, k, oid, v)
if !oid.Equals(v) {
t.Errorf("invalid oid for #%v %v/%v:\ngot:\n%#v\nexpected:\n%#v", caseIndex, c.format.ObjectFormat, k, oid.UIString(), v.UIString())
}
rc, err := repo.Open(oid)
@@ -540,8 +481,8 @@ func TestInvalidEncryptionKey(t *testing.T) {
data := map[string][]byte{}
st := storagetesting.NewMapStorage(data)
format := Format{
Version: "1",
ObjectFormat: "sha512-aes256",
Version: 1,
ObjectFormat: ObjectIDFormat_ENCRYPTED_HMAC_SHA512_384_AES256,
Secret: []byte("key"),
MaxBlobSize: 1000,
}
@@ -571,47 +512,42 @@ func TestInvalidEncryptionKey(t *testing.T) {
}
// Key too short
rc, err = repo.Open(oid[0 : len(oid)-2])
rc, err = repo.Open(replaceEncryption(oid, oid.Encryption[0:len(oid.Encryption)-2]))
if err == nil || rc != nil {
t.Errorf("expected error when opening malformed object")
}
// Key too long
rc, err = repo.Open(oid + "ff")
rc, err = repo.Open(replaceEncryption(oid, append(oid.Encryption, 0xFF)))
if err == nil || rc != nil {
t.Errorf("expected error when opening malformed object")
}
// Invalid key
lastByte, _ := hex.DecodeString(string(oid[len(oid)-2:]))
lastByte[0]++
rc, err = repo.Open(oid[0:len(oid)-2] + ObjectID(hex.EncodeToString(lastByte)))
corruptedKey := append([]byte(nil), oid.Encryption...)
corruptedKey[0]++
rc, err = repo.Open(replaceEncryption(oid, corruptedKey))
if err == nil || rc != nil {
t.Errorf("expected error when opening malformed object: %v", err)
}
// Now corrupt the data
data[string(oid.BlockID())][0] ^= 1
data[oid.StorageBlock][0] ^= 1
rc, err = repo.Open(oid)
if err == nil || rc != nil {
t.Errorf("expected error when opening object with corrupt data")
}
}
func verifyStorageBlocks(t *testing.T, actual []string, data, expectedData map[string][]byte) {
actualCopy := append([]string(nil), actual...)
sort.Strings(actualCopy)
var expected []string
for k := range expectedData {
expected = append(expected, k)
}
sort.Strings(expected)
if !reflect.DeepEqual(expected, actualCopy) {
t.Errorf("updated blocks don't match. Expected: %#v, got %#v", expected, actualCopy)
}
if !reflect.DeepEqual(expectedData, data) {
t.Errorf("storage data does not match, expected: %v, got %v", expectedData, data)
}
func replaceEncryption(oid ObjectID, newEncryption []byte) ObjectID {
oid.Encryption = newEncryption
return oid
}
func mustParseBase16(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic("invalid hex literal: " + s)
}
return b
}
+21 -16
View File
@@ -6,6 +6,8 @@ import (
"encoding/hex"
"fmt"
"github.com/golang/protobuf/proto"
"github.com/kopia/kopia/repo"
)
@@ -16,14 +18,14 @@ const (
storedObjectIDLengthBytes = 8
)
type objectIDData struct {
ObjectID string `json:"objectID"`
}
// SaveObjectID stores the given object ID in an encrypted vault item and returns a unique ID.
func (vlt *Vault) SaveObjectID(oid repo.ObjectID) (string, error) {
h := hmac.New(sha256.New, vlt.format.UniqueID)
h.Write([]byte(oid))
bytes, err := proto.Marshal(&oid)
if err != nil {
return "", err
}
h.Write(bytes)
sum := h.Sum(nil)
for i := storedObjectIDLengthBytes; i < len(sum); i++ {
sum[i%storedObjectIDLengthBytes] ^= sum[i]
@@ -31,10 +33,7 @@ func (vlt *Vault) SaveObjectID(oid repo.ObjectID) (string, error) {
sum = sum[0:storedObjectIDLengthBytes]
key := StoredObjectIDPrefix + hex.EncodeToString(sum)
var d objectIDData
d.ObjectID = string(oid)
if err := vlt.putJSON(key, &d); err != nil {
if err := vlt.Put(key, bytes); err != nil {
return "", err
}
@@ -45,20 +44,26 @@ func (vlt *Vault) SaveObjectID(oid repo.ObjectID) (string, error) {
func (vlt *Vault) GetObjectID(id string) (repo.ObjectID, error) {
matches, err := vlt.List(id)
if err != nil {
return "", err
return repo.NullObjectID, err
}
switch len(matches) {
case 0:
return "", ErrItemNotFound
return repo.NullObjectID, ErrItemNotFound
case 1:
var d objectIDData
if err := vlt.getJSON(matches[0], &d); err != nil {
return "", err
b, err := vlt.Get(matches[0])
if err != nil {
return repo.NullObjectID, err
}
return repo.ParseObjectID(d.ObjectID)
var oid repo.ObjectID
if err := proto.Unmarshal(b, &oid); err != nil {
return repo.NullObjectID, err
}
return oid, nil
default:
return "", fmt.Errorf("ambiguous object ID: %v", id)
return repo.NullObjectID, fmt.Errorf("ambiguous object ID: %v", id)
}
}
+35
View File
@@ -0,0 +1,35 @@
// Code generated by protoc-gen-go.
// source: vault/vault.proto
// DO NOT EDIT!
/*
Package vault is a generated protocol buffer package.
It is generated from these files:
vault/vault.proto
It has these top-level messages:
*/
package vault
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
const _ = proto.ProtoPackageIsVersion1
var fileDescriptor0 = []byte{
// 75 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0x2c, 0x4b, 0x2c, 0xcd,
0x29, 0xd1, 0x07, 0x93, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0xdc, 0xd9, 0xf9, 0x05, 0x99,
0x89, 0x7a, 0x60, 0x21, 0x27, 0xb9, 0x28, 0x99, 0xf4, 0xcc, 0x92, 0x8c, 0xd2, 0x24, 0xbd, 0xe4,
0xfc, 0x5c, 0x7d, 0xb0, 0x0c, 0x94, 0x04, 0xcb, 0x27, 0xb1, 0x81, 0xf5, 0x18, 0x03, 0x02, 0x00,
0x00, 0xff, 0xff, 0x9a, 0x15, 0x10, 0x31, 0x48, 0x00, 0x00, 0x00,
}
+4
View File
@@ -0,0 +1,4 @@
syntax = "proto3";
package kopia.vault;
option go_package = "github.com/kopia/kopia/vault";
+4 -4
View File
@@ -62,9 +62,9 @@ func verifyVault(t *testing.T, vaultPath string, repoPath string) {
}
repoFormat := &repo.Format{
Version: "1",
Version: 1,
MaxBlobSize: 1000000,
ObjectFormat: "sha256",
ObjectFormat: repo.ObjectIDFormat_UNENCYPTED_HMAC_SHA256,
Secret: []byte{1, 2, 3},
}
@@ -189,7 +189,7 @@ func verifyVault(t *testing.T, vaultPath string, repoPath string) {
if err != nil {
t.Errorf("error getting object ID: %v", err)
} else {
if oid != oid1 {
if !oid.Equals(oid1) {
t.Errorf("got invalid object ID: %v expected %v", oid, oid1)
}
}
@@ -198,7 +198,7 @@ func verifyVault(t *testing.T, vaultPath string, repoPath string) {
if err != nil {
t.Errorf("error getting object ID: %v", err)
} else {
if oid != oid2 {
if !oid.Equals(oid2) {
t.Errorf("got invalid object ID: %v expected %v", oid, oid2)
}
}