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 }