mirror of
https://github.com/kopia/kopia.git
synced 2026-01-26 07:18:02 -05:00
202 lines
4.5 KiB
Go
202 lines
4.5 KiB
Go
package object
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"fmt"
|
|
)
|
|
|
|
// ID is an identifier of a repository object. Repository objects can be stored:
|
|
//
|
|
// 1. In a single storage block, this is the most common case for objects up to typically ~20MB.
|
|
// Storage blocks are encrypted with key specified in EncryptionKey.
|
|
//
|
|
// 2. In a series of storage blocks with an indirect block pointing at them (multiple indirections are allowed). This is used for larger files.
|
|
//
|
|
// 3. Packed into larger objects (packs).
|
|
//
|
|
// ObjectIDs have standard string representation (returned by String() and accepted as input to ParseObjectID()) suitable for using
|
|
// in user interfaces, such as command-line tools:
|
|
//
|
|
// Examples:
|
|
//
|
|
// "D295754edeb35c17911b1fdf853f572fe" // storage block
|
|
// "ID2c33acbcba3569f943d9e8aaea7817c5" // level-1 indirection block
|
|
// "IID2c33acbcba3569f943d9e8aaea7817c5" // level-2 indirection block
|
|
// "S30,50,D295754edeb35c17911b1fdf853f572fe" // section of "D295754edeb35c17911b1fdf853f572fe" between [30,80)
|
|
//
|
|
//
|
|
type ID struct {
|
|
StorageBlock string
|
|
Indirect *ID
|
|
}
|
|
|
|
// MarshalJSON emits ObjectID in standard string format.
|
|
func (oid *ID) MarshalJSON() ([]byte, error) {
|
|
s := oid.String()
|
|
return json.Marshal(&s)
|
|
}
|
|
|
|
// UnmarshalJSON unmarshals Object ID from a JSON string.
|
|
func (oid *ID) UnmarshalJSON(b []byte) error {
|
|
var s string
|
|
err := json.Unmarshal(b, &s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*oid, err = ParseID(s)
|
|
return err
|
|
}
|
|
|
|
// HasObjectID exposes the identifier of an object.
|
|
type HasObjectID interface {
|
|
ObjectID() ID
|
|
}
|
|
|
|
// NullID is the identifier of an null/empty object.
|
|
var NullID ID
|
|
|
|
var (
|
|
inlineContentEncoding = base64.RawURLEncoding
|
|
)
|
|
|
|
// String returns string representation of ObjectID that is suitable for displaying in the UI.
|
|
//
|
|
// Note that the object ID name often contains its encryption key, which is sensitive and can be quite long (~100 characters long).
|
|
func (oid ID) String() string {
|
|
if oid.Indirect != nil {
|
|
return fmt.Sprintf("I%v", oid.Indirect)
|
|
}
|
|
|
|
if oid.StorageBlock != "" {
|
|
return "D" + oid.StorageBlock
|
|
}
|
|
|
|
return "B"
|
|
}
|
|
|
|
// Validate validates the ObjectID structure.
|
|
func (oid *ID) Validate() error {
|
|
var c int
|
|
if len(oid.StorageBlock) > 0 {
|
|
c++
|
|
}
|
|
|
|
if oid.Indirect != nil {
|
|
c++
|
|
if err := oid.Indirect.Validate(); err != nil {
|
|
return fmt.Errorf("invalid indirect object ID %v: %v", oid, err)
|
|
}
|
|
}
|
|
|
|
if c != 1 {
|
|
return fmt.Errorf("inconsistent block content: %+v", oid)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseNumberUntilComma parses a string of the form "{x},{remainder}" where x is a 64-bit number and remainder is arbitrary string.
|
|
// Returns the number and remainder.
|
|
func parseNumberUntilComma(s string) (int64, string, error) {
|
|
comma := strings.IndexByte(s, ',')
|
|
if comma < 0 {
|
|
return 0, "", errors.New("missing comma")
|
|
}
|
|
|
|
num, err := strconv.ParseInt(s[0:comma], 10, 64)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
return num, s[comma+1:], nil
|
|
}
|
|
|
|
func parseSectionInfoString(s string) (int64, int64, ID, error) {
|
|
var start, length int64
|
|
var err error
|
|
|
|
start, s, err = parseNumberUntilComma(s[1:])
|
|
if err != nil {
|
|
return 0, -1, NullID, err
|
|
}
|
|
|
|
length, s, err = parseNumberUntilComma(s)
|
|
if err != nil {
|
|
return 0, -1, NullID, err
|
|
}
|
|
|
|
oid, err := ParseID(s)
|
|
if err != nil {
|
|
return 0, -1, NullID, err
|
|
}
|
|
|
|
return start, length, oid, nil
|
|
}
|
|
|
|
// ParseID converts the specified string into ObjectID.
|
|
// The string format matches the output of String() method.
|
|
func ParseID(s string) (ID, error) {
|
|
if len(s) >= 1 {
|
|
chunkType := s[0]
|
|
content := s[1:]
|
|
|
|
switch chunkType {
|
|
case 'P':
|
|
// legacy
|
|
parts := strings.Split(content, "@")
|
|
if len(parts) == 2 && len(parts[0]) > 0 && len(parts[1]) > 0 {
|
|
return ID{
|
|
StorageBlock: parts[0],
|
|
}, nil
|
|
}
|
|
|
|
case 'I', 'D':
|
|
if chunkType == 'I' {
|
|
if len(content) < 2 || content[1] != ',' {
|
|
base, err := ParseID(content)
|
|
if err != nil {
|
|
return NullID, err
|
|
}
|
|
|
|
return ID{Indirect: &base}, nil
|
|
}
|
|
|
|
// legacy
|
|
comma := strings.Index(content, ",")
|
|
if comma < 0 {
|
|
// malformed
|
|
break
|
|
}
|
|
indirectLevel, err := strconv.Atoi(content[0:comma])
|
|
if err != nil {
|
|
break
|
|
}
|
|
if indirectLevel <= 0 {
|
|
break
|
|
}
|
|
content = content[comma+1:]
|
|
if content == "" {
|
|
break
|
|
}
|
|
|
|
o := &ID{StorageBlock: content}
|
|
for i := 0; i < indirectLevel; i++ {
|
|
o = &ID{Indirect: o}
|
|
}
|
|
|
|
return *o, nil
|
|
}
|
|
|
|
return ID{StorageBlock: content}, nil
|
|
}
|
|
}
|
|
|
|
return NullID, fmt.Errorf("malformed object id: '%s'", s)
|
|
}
|