github.com/creachadair/ffs@v0.17.3/storage/filestore/filestore.go (about)

     1  // Copyright 2019 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 filestore implements the [blob.KV] interface using files.  The store
    16  // comprises a directory with subdirectories keyed by a prefix of the encoded
    17  // blob key.
    18  package filestore
    19  
    20  import (
    21  	"context"
    22  	"encoding/hex"
    23  	"errors"
    24  	"fmt"
    25  	"iter"
    26  	"os"
    27  	"path"
    28  	"path/filepath"
    29  	"sort"
    30  	"strings"
    31  
    32  	"github.com/creachadair/atomicfile"
    33  	"github.com/creachadair/ffs/blob"
    34  	"github.com/creachadair/ffs/storage/hexkey"
    35  )
    36  
    37  // Store implements the [blob.Store] interface using a directory structure with
    38  // one file per stored blob. Keys are encoded in hex and used to construct the
    39  // file and directory names relative to a root directory, similar to a Git
    40  // local object store.
    41  type Store struct {
    42  	key hexkey.Config
    43  }
    44  
    45  // New creates a Store associated with the specified root directory, which is
    46  // created if it does not already exist.
    47  func New(dir string) (Store, error) {
    48  	path := filepath.Clean(dir)
    49  	if err := os.MkdirAll(path, 0700); err != nil {
    50  		return Store{}, err
    51  	}
    52  	return Store{key: hexkey.Config{Prefix: path, Shard: 3}}, nil
    53  }
    54  
    55  func (s Store) mkPath(name string) (string, error) {
    56  	if name == "" {
    57  		return s.key.Prefix, nil // already known to exist
    58  	}
    59  	// Prefix non-empty name with "_" to avert conflict with hex keys.
    60  	path := filepath.Join(s.key.Prefix, "_"+hex.EncodeToString([]byte(name)))
    61  	return path, os.MkdirAll(path, 0700)
    62  }
    63  
    64  // KV implements part of the [blob.Store] interface.
    65  func (s Store) KV(_ context.Context, name string) (blob.KV, error) {
    66  	path, err := s.mkPath(name)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	return KV{key: s.key.WithPrefix(path)}, nil
    71  }
    72  
    73  // CAS implements part of the [blob.Store] interface.
    74  func (s Store) CAS(ctx context.Context, name string) (blob.CAS, error) {
    75  	return blob.CASFromKVError(s.KV(ctx, name))
    76  }
    77  
    78  // Sub implements part of the [blob.Store] interface.
    79  func (s Store) Sub(_ context.Context, name string) (blob.Store, error) {
    80  	path, err := s.mkPath(name)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	return Store{key: s.key.WithPrefix(path)}, nil
    85  }
    86  
    87  // Close implements part of the [blob.StoreCloser] interface.
    88  // This implementation always reports nil.
    89  func (Store) Close(context.Context) error { return nil }
    90  
    91  // KV implements the [blob.kV] interface using a directory structure with one
    92  // file per stored blob. Keys are encoded in hex and used to construct file and
    93  // directory names relative to a root directory, similar to a Git local object
    94  // store.
    95  type KV struct {
    96  	key hexkey.Config
    97  }
    98  
    99  // Opener constructs a filestore from an address comprising a path, for use
   100  // with the [store] package. The concrete type of the result is [Store].
   101  //
   102  // [store]: https://godoc.org/github.com/creachadair/ffstools/lib/store
   103  func Opener(ctx context.Context, addr string) (blob.StoreCloser, error) {
   104  	return New(strings.TrimPrefix(addr, "//")) // allow URL-like paths
   105  }
   106  
   107  func (s KV) keyPath(key string) string { return s.key.Encode(key) }
   108  
   109  // Get implements part of [blob.KV]. It linearizes to the point at which
   110  // opening the key path for reading returns.
   111  func (s KV) Get(_ context.Context, key string) ([]byte, error) {
   112  	bits, err := os.ReadFile(s.keyPath(key))
   113  	if err != nil {
   114  		if errors.Is(err, os.ErrNotExist) {
   115  			err = blob.KeyNotFound(key)
   116  		}
   117  		return nil, fmt.Errorf("key %q: %w", key, err)
   118  	}
   119  	return bits, nil
   120  }
   121  
   122  // Has implements part of [blob.KV].
   123  func (s KV) Has(ctx context.Context, keys ...string) (blob.KeySet, error) {
   124  	var out blob.KeySet
   125  	for _, key := range keys {
   126  		if _, err := os.Stat(s.keyPath(key)); err == nil {
   127  			out.Add(key)
   128  		} else if !errors.Is(err, os.ErrNotExist) {
   129  			return nil, fmt.Errorf("key %q: %w", key, err)
   130  		}
   131  	}
   132  	return out, nil
   133  }
   134  
   135  // Put implements part of [blob.KV]. A successful Put linearizes to the point
   136  // at which the rename of the write temporary succeeds; a Put that fails due to
   137  // an existing key linearizes to the point when the key path stat succeeds.
   138  func (s KV) Put(_ context.Context, opts blob.PutOptions) error {
   139  	path := s.keyPath(opts.Key)
   140  	if _, err := os.Stat(path); err == nil && !opts.Replace {
   141  		return blob.KeyExists(opts.Key)
   142  	} else if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
   143  		return err
   144  	}
   145  	return atomicfile.WriteData(path, opts.Data, 0600)
   146  }
   147  
   148  // Delete implements part of [blob.KV].
   149  func (s KV) Delete(_ context.Context, key string) error {
   150  	path := s.keyPath(key)
   151  	err := os.Remove(path)
   152  	if os.IsNotExist(err) {
   153  		return blob.KeyNotFound(key)
   154  	}
   155  	return err
   156  }
   157  
   158  // List implements part of [blob.KV]. If any concurrent Put operation on a key
   159  // later than the current scan position succeeds, List linearizes immediately
   160  // prior to the earliest such Put operation. Otherwise, List may be linearized
   161  // to any point during its execution.
   162  func (s KV) List(_ context.Context, start string) iter.Seq2[string, error] {
   163  	return func(yield func(string, error) bool) {
   164  		roots, err := listdir(s.Dir())
   165  		if err != nil {
   166  			yield("", err)
   167  			return // regardless
   168  		}
   169  		for _, root := range roots {
   170  			cur := filepath.Join(s.Dir(), root)
   171  			keys, err := listdir(cur)
   172  			if err != nil {
   173  				yield("", err)
   174  				return
   175  			}
   176  			for _, tail := range keys {
   177  				key, err := s.key.Decode(path.Join(cur, tail))
   178  				if err != nil || key < start {
   179  					continue // skip non-key files and keys prior to the start
   180  				}
   181  				if !yield(key, nil) {
   182  					return
   183  				}
   184  			}
   185  		}
   186  	}
   187  }
   188  
   189  // Len implements part of [blob.KV]. It is implemented using List, so it
   190  // linearizes in the same manner.
   191  func (s KV) Len(ctx context.Context) (int64, error) {
   192  	var nb int64
   193  	for _, err := range s.List(ctx, "") {
   194  		if err != nil {
   195  			return 0, err
   196  		}
   197  		nb++
   198  	}
   199  	return nb, nil
   200  }
   201  
   202  // Dir reports the directory path associated with s.
   203  func (s KV) Dir() string { return s.key.Prefix }
   204  
   205  func listdir(path string) ([]string, error) {
   206  	f, err := os.Open(path)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	names, err := f.Readdirnames(-1)
   211  	f.Close()
   212  	sort.Strings(names)
   213  	return names, err
   214  }