github.com/uber/kraken@v0.1.4/lib/store/ca_store.go (about) 1 // Copyright (c) 2016-2019 Uber Technologies, Inc. 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 package store 15 16 import ( 17 "fmt" 18 "hash" 19 "io" 20 "os" 21 "path" 22 23 "github.com/uber/kraken/core" 24 "github.com/uber/kraken/lib/hrw" 25 "github.com/uber/kraken/lib/store/base" 26 "github.com/andres-erbsen/clock" 27 "github.com/docker/distribution/uuid" 28 "github.com/spaolacci/murmur3" 29 "github.com/uber-go/tally" 30 ) 31 32 // CAStore allows uploading / caching content-addressable files. 33 type CAStore struct { 34 *uploadStore 35 *cacheStore 36 cleanup *cleanupManager 37 } 38 39 // NewCAStore creates a new CAStore. 40 func NewCAStore(config CAStoreConfig, stats tally.Scope) (*CAStore, error) { 41 config = config.applyDefaults() 42 43 stats = stats.Tagged(map[string]string{ 44 "module": "castore", 45 }) 46 47 uploadStore, err := newUploadStore(config.UploadDir) 48 if err != nil { 49 return nil, fmt.Errorf("new upload store: %s", err) 50 } 51 52 cacheBackend := base.NewCASFileStoreWithLRUMap(config.Capacity, clock.New()) 53 cacheStore, err := newCacheStore(config.CacheDir, cacheBackend) 54 if err != nil { 55 return nil, fmt.Errorf("new cache store: %s", err) 56 } 57 58 if err := initCASVolumes(config.CacheDir, config.Volumes); err != nil { 59 return nil, fmt.Errorf("init cas volumes: %s", err) 60 } 61 62 cleanup, err := newCleanupManager(clock.New(), stats) 63 if err != nil { 64 return nil, fmt.Errorf("new cleanup manager: %s", err) 65 } 66 cleanup.addJob("upload", config.UploadCleanup, uploadStore.newFileOp()) 67 cleanup.addJob("cache", config.CacheCleanup, cacheStore.newFileOp()) 68 69 return &CAStore{uploadStore, cacheStore, cleanup}, nil 70 } 71 72 // Close terminates any goroutines started by s. 73 func (s *CAStore) Close() { 74 s.cleanup.stop() 75 } 76 77 // MoveUploadFileToCache commits uploadName as cacheName. Clients are expected 78 // to validate the content of the upload file matches the cacheName digest. 79 func (s *CAStore) MoveUploadFileToCache(uploadName, cacheName string) error { 80 uploadPath, err := s.uploadStore.newFileOp().GetFilePath(uploadName) 81 if err != nil { 82 return err 83 } 84 defer s.DeleteUploadFile(uploadName) 85 return s.cacheStore.newFileOp().MoveFileFrom(cacheName, s.cacheStore.state, uploadPath) 86 } 87 88 // CreateCacheFile initializes a cache file for name from r. name should be a raw 89 // hex sha256 digest, and the contents of r must hash to name. 90 func (s *CAStore) CreateCacheFile(name string, r io.Reader) error { 91 return s.WriteCacheFile(name, func(w FileReadWriter) error { 92 _, err := io.Copy(w, r) 93 return err 94 }) 95 } 96 97 // WriteCacheFile initializes a cache file for name by passing a temporary 98 // upload file writer to the write function. 99 func (s *CAStore) WriteCacheFile(name string, write func(w FileReadWriter) error) error { 100 tmp := fmt.Sprintf("%s.%s", name, uuid.Generate().String()) 101 if err := s.CreateUploadFile(tmp, 0); err != nil { 102 return fmt.Errorf("create upload file: %s", err) 103 } 104 defer s.DeleteUploadFile(tmp) 105 106 w, err := s.GetUploadFileReadWriter(tmp) 107 if err != nil { 108 return fmt.Errorf("get upload writer: %s", err) 109 } 110 defer w.Close() 111 112 if err := write(w); err != nil { 113 return err 114 } 115 116 if _, err := w.Seek(0, 0); err != nil { 117 return fmt.Errorf("seek: %s", err) 118 } 119 actual, err := core.NewDigester().FromReader(w) 120 if err != nil { 121 return fmt.Errorf("compute digest: %s", err) 122 } 123 expected, err := core.NewSHA256DigestFromHex(name) 124 if err != nil { 125 return fmt.Errorf("new digest from file name: %s", err) 126 } 127 if actual != expected { 128 return fmt.Errorf("failed to verify data: digests do not match") 129 } 130 131 if err := s.MoveUploadFileToCache(tmp, name); err != nil && !os.IsExist(err) { 132 return fmt.Errorf("move upload file to cache: %s", err) 133 } 134 return nil 135 } 136 137 func initCASVolumes(dir string, volumes []Volume) error { 138 if len(volumes) == 0 { 139 return nil 140 } 141 142 rendezvousHash := hrw.NewRendezvousHash( 143 func() hash.Hash { return murmur3.New64() }, 144 hrw.UInt64ToFloat64) 145 146 for _, v := range volumes { 147 if _, err := os.Stat(v.Location); err != nil { 148 return fmt.Errorf("verify volume: %s", err) 149 } 150 rendezvousHash.AddNode(v.Location, v.Weight) 151 } 152 153 // Create 256 symlinks under dir. 154 for subdirIndex := 0; subdirIndex < 256; subdirIndex++ { 155 subdirName := fmt.Sprintf("%02X", subdirIndex) 156 nodes := rendezvousHash.GetOrderedNodes(subdirName, 1) 157 if len(nodes) != 1 { 158 return fmt.Errorf("calculate volume for subdir: %s", subdirName) 159 } 160 sourcePath := path.Join(nodes[0].Label, path.Base(dir), subdirName) 161 if err := os.MkdirAll(sourcePath, 0775); err != nil { 162 return fmt.Errorf("volume source path: %s", err) 163 } 164 targetPath := path.Join(dir, subdirName) 165 if err := createOrUpdateSymlink(sourcePath, targetPath); err != nil { 166 return fmt.Errorf("symlink to volume: %s", err) 167 } 168 } 169 170 return nil 171 }