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  }