github.com/creachadair/ffs@v0.17.3/file/root/root.go (about) 1 // Copyright 2021 Michael J. Fromberger. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package root defines a storage representation for pointers to file trees and 16 // associated metadata. 17 package root 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 24 "github.com/creachadair/ffs/blob" 25 "github.com/creachadair/ffs/file" 26 "github.com/creachadair/ffs/file/wiretype" 27 "google.golang.org/protobuf/proto" 28 ) 29 30 // ErrNoData indicates that the requested data do not exist. 31 var ErrNoData = errors.New("requested data not found") 32 33 // A Root records the location of the root of a file tree. 34 type Root struct { 35 kv blob.KV 36 37 Description string // a human-readable description 38 FileKey string // the storage key of the file node 39 IndexKey string // the storage key of the blob index 40 ChainKey string // the storage key of a predecessor root 41 } 42 43 // New constructs a new empty Root associated with the given store. 44 // If opts != nil, initial values are set from its contents. 45 func New(s blob.KV, opts *Options) *Root { 46 if opts == nil { 47 opts = new(Options) 48 } 49 return &Root{ 50 kv: s, 51 52 Description: opts.Description, 53 FileKey: opts.FileKey, 54 IndexKey: opts.IndexKey, 55 } 56 } 57 58 // Open opens a stored root record given its storage key in s. 59 func Open(ctx context.Context, s blob.KV, key string) (*Root, error) { 60 var obj wiretype.Object 61 if err := wiretype.Load(ctx, s, key, &obj); err != nil { 62 return nil, fmt.Errorf("loading root %q: %w", key, err) 63 } 64 return Decode(s, &obj) 65 } 66 67 // File loads and returns the root file of r from s, if one exists. If no file 68 // exists, it returns [ErrNoData]. 69 func (r *Root) File(ctx context.Context, s blob.CAS) (*file.File, error) { 70 if r.FileKey == "" { 71 return nil, ErrNoData 72 } else if s == nil { 73 return nil, errors.New("no store provided") 74 } 75 return file.Open(ctx, s, r.FileKey) 76 } 77 78 // Save writes r in wire format to the given storage key. 79 func (r *Root) Save(ctx context.Context, key string) error { 80 if r.FileKey == "" { 81 return errors.New("missing file key") 82 } 83 bits, err := proto.Marshal(Encode(r)) 84 if err != nil { 85 return err 86 } 87 return r.kv.Put(ctx, blob.PutOptions{ 88 Key: key, 89 Data: bits, 90 Replace: true, 91 }) 92 } 93 94 // Chain loads and returns the chained root of r from s, if one exists. 95 // If no chained root exists, it returns [ErrNoData]. 96 func (r *Root) Chain(ctx context.Context, s wiretype.Getter) (*Root, error) { 97 if r.ChainKey == "" { 98 return nil, ErrNoData 99 } else if s == nil { 100 return nil, errors.New("no store provided") 101 } 102 var obj wiretype.Object 103 if err := wiretype.Load(ctx, s, r.ChainKey, &obj); err != nil { 104 return nil, fmt.Errorf("loading chained root: %w", err) 105 } 106 // Note when we construct a new root, we need to give it our KV, not the CAS 107 // we loaded from. 108 return Decode(r.kv, &obj) 109 } 110 111 // SaveChain writes r in wire format to the specified [blob.CAS], and returns 112 // its storage key. 113 func (r *Root) SaveChain(ctx context.Context, s blob.CAS) (string, error) { 114 if s == nil { 115 return "", errors.New("no store provided") 116 } 117 bits, err := proto.Marshal(Encode(r)) 118 if err != nil { 119 return "", err 120 } 121 return s.CASPut(ctx, bits) 122 } 123 124 // Encode encodes r as a protobuf message for storage. 125 func Encode(r *Root) *wiretype.Object { 126 return &wiretype.Object{ 127 Value: &wiretype.Object_Root{ 128 Root: &wiretype.Root{ 129 FileKey: []byte(r.FileKey), 130 Description: r.Description, 131 IndexKey: []byte(r.IndexKey), 132 ChainKey: []byte(r.ChainKey), 133 }, 134 }, 135 } 136 } 137 138 // Decode decodes a protobuf-encoded root record and associates it with the 139 // storage in s. 140 func Decode(s blob.KV, obj *wiretype.Object) (*Root, error) { 141 pb, ok := obj.Value.(*wiretype.Object_Root) 142 if !ok { 143 return nil, errors.New("object does not contain a root") 144 } 145 return &Root{ 146 kv: s, 147 148 Description: pb.Root.Description, 149 FileKey: string(pb.Root.FileKey), 150 IndexKey: string(pb.Root.IndexKey), 151 ChainKey: string(pb.Root.ChainKey), 152 }, nil 153 } 154 155 // Options are configurable settings for creating a Root. A nil options 156 // pointer provides zero values for all fields. 157 type Options struct { 158 FileKey string 159 Description string 160 IndexKey string 161 ChainKey string 162 }