github.com/safing/portbase@v0.19.5/database/storage/fstree/fstree.go (about) 1 /* 2 Package fstree provides a dead simple file-based database storage backend. 3 It is primarily meant for easy testing or storing big files that can easily be accesses directly, without datastore. 4 */ 5 package fstree 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "io/fs" 12 "os" 13 "path/filepath" 14 "runtime" 15 "strings" 16 "time" 17 18 "github.com/safing/portbase/database/iterator" 19 "github.com/safing/portbase/database/query" 20 "github.com/safing/portbase/database/record" 21 "github.com/safing/portbase/database/storage" 22 "github.com/safing/portbase/utils/renameio" 23 ) 24 25 const ( 26 defaultFileMode = os.FileMode(0o0644) 27 defaultDirMode = os.FileMode(0o0755) 28 onWindows = runtime.GOOS == "windows" 29 ) 30 31 // FSTree database storage. 32 type FSTree struct { 33 name string 34 basePath string 35 } 36 37 func init() { 38 _ = storage.Register("fstree", NewFSTree) 39 } 40 41 // NewFSTree returns a (new) FSTree database. 42 func NewFSTree(name, location string) (storage.Interface, error) { 43 basePath, err := filepath.Abs(location) 44 if err != nil { 45 return nil, fmt.Errorf("fstree: failed to validate path %s: %w", location, err) 46 } 47 48 file, err := os.Stat(basePath) 49 if err != nil { 50 if errors.Is(err, fs.ErrNotExist) { 51 err = os.MkdirAll(basePath, defaultDirMode) 52 if err != nil { 53 return nil, fmt.Errorf("fstree: failed to create directory %s: %w", basePath, err) 54 } 55 } else { 56 return nil, fmt.Errorf("fstree: failed to stat path %s: %w", basePath, err) 57 } 58 } else { 59 if !file.IsDir() { 60 return nil, fmt.Errorf("fstree: provided database path (%s) is a file", basePath) 61 } 62 } 63 64 return &FSTree{ 65 name: name, 66 basePath: basePath, 67 }, nil 68 } 69 70 func (fst *FSTree) buildFilePath(key string, checkKeyLength bool) (string, error) { 71 // check key length 72 if checkKeyLength && len(key) < 1 { 73 return "", fmt.Errorf("fstree: key too short: %s", key) 74 } 75 // build filepath 76 dstPath := filepath.Join(fst.basePath, key) // Join also calls Clean() 77 if !strings.HasPrefix(dstPath, fst.basePath) { 78 return "", fmt.Errorf("fstree: key integrity check failed, compiled path is %s", dstPath) 79 } 80 // return 81 return dstPath, nil 82 } 83 84 // Get returns a database record. 85 func (fst *FSTree) Get(key string) (record.Record, error) { 86 dstPath, err := fst.buildFilePath(key, true) 87 if err != nil { 88 return nil, err 89 } 90 91 data, err := os.ReadFile(dstPath) 92 if err != nil { 93 if errors.Is(err, fs.ErrNotExist) { 94 return nil, storage.ErrNotFound 95 } 96 return nil, fmt.Errorf("fstree: failed to read file %s: %w", dstPath, err) 97 } 98 99 r, err := record.NewRawWrapper(fst.name, key, data) 100 if err != nil { 101 return nil, err 102 } 103 return r, nil 104 } 105 106 // GetMeta returns the metadata of a database record. 107 func (fst *FSTree) GetMeta(key string) (*record.Meta, error) { 108 // TODO: Replace with more performant variant. 109 110 r, err := fst.Get(key) 111 if err != nil { 112 return nil, err 113 } 114 115 return r.Meta(), nil 116 } 117 118 // Put stores a record in the database. 119 func (fst *FSTree) Put(r record.Record) (record.Record, error) { 120 dstPath, err := fst.buildFilePath(r.DatabaseKey(), true) 121 if err != nil { 122 return nil, err 123 } 124 125 data, err := r.MarshalRecord(r) 126 if err != nil { 127 return nil, err 128 } 129 130 err = writeFile(dstPath, data, defaultFileMode) 131 if err != nil { 132 // create dir and try again 133 err = os.MkdirAll(filepath.Dir(dstPath), defaultDirMode) 134 if err != nil { 135 return nil, fmt.Errorf("fstree: failed to create directory %s: %w", filepath.Dir(dstPath), err) 136 } 137 err = writeFile(dstPath, data, defaultFileMode) 138 if err != nil { 139 return nil, fmt.Errorf("fstree: could not write file %s: %w", dstPath, err) 140 } 141 } 142 143 return r, nil 144 } 145 146 // Delete deletes a record from the database. 147 func (fst *FSTree) Delete(key string) error { 148 dstPath, err := fst.buildFilePath(key, true) 149 if err != nil { 150 return err 151 } 152 153 // remove entry 154 err = os.Remove(dstPath) 155 if err != nil { 156 return fmt.Errorf("fstree: could not delete %s: %w", dstPath, err) 157 } 158 159 return nil 160 } 161 162 // Query returns a an iterator for the supplied query. 163 func (fst *FSTree) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) { 164 _, err := q.Check() 165 if err != nil { 166 return nil, fmt.Errorf("invalid query: %w", err) 167 } 168 169 walkPrefix, err := fst.buildFilePath(q.DatabaseKeyPrefix(), false) 170 if err != nil { 171 return nil, err 172 } 173 fileInfo, err := os.Stat(walkPrefix) 174 var walkRoot string 175 switch { 176 case err == nil && fileInfo.IsDir(): 177 walkRoot = walkPrefix 178 case err == nil: 179 walkRoot = filepath.Dir(walkPrefix) 180 case errors.Is(err, fs.ErrNotExist): 181 walkRoot = filepath.Dir(walkPrefix) 182 default: // err != nil 183 return nil, fmt.Errorf("fstree: could not stat query root %s: %w", walkPrefix, err) 184 } 185 186 queryIter := iterator.New() 187 188 go fst.queryExecutor(walkRoot, queryIter, q, local, internal) 189 return queryIter, nil 190 } 191 192 func (fst *FSTree) queryExecutor(walkRoot string, queryIter *iterator.Iterator, q *query.Query, local, internal bool) { 193 err := filepath.Walk(walkRoot, func(path string, info os.FileInfo, err error) error { 194 if err != nil { 195 return fmt.Errorf("fstree: error in walking fs: %w", err) 196 } 197 198 if info.IsDir() { 199 // skip dir if not in scope 200 if !strings.HasPrefix(path, fst.basePath) { 201 return filepath.SkipDir 202 } 203 // continue 204 return nil 205 } 206 207 // still in scope? 208 if !strings.HasPrefix(path, fst.basePath) { 209 return nil 210 } 211 212 // read file 213 data, err := os.ReadFile(path) 214 if err != nil { 215 if errors.Is(err, fs.ErrNotExist) { 216 return nil 217 } 218 return fmt.Errorf("fstree: failed to read file %s: %w", path, err) 219 } 220 221 // parse 222 key, err := filepath.Rel(fst.basePath, path) 223 if err != nil { 224 return fmt.Errorf("fstree: failed to extract key from filepath %s: %w", path, err) 225 } 226 r, err := record.NewRawWrapper(fst.name, key, data) 227 if err != nil { 228 return fmt.Errorf("fstree: failed to load file %s: %w", path, err) 229 } 230 231 if !r.Meta().CheckValidity() { 232 // record is not valid 233 return nil 234 } 235 236 if !r.Meta().CheckPermission(local, internal) { 237 // no permission to access 238 return nil 239 } 240 241 // check if matches, then send 242 if q.MatchesRecord(r) { 243 select { 244 case queryIter.Next <- r: 245 case <-queryIter.Done: 246 case <-time.After(1 * time.Second): 247 return errors.New("fstree: query buffer full, timeout") 248 } 249 } 250 251 return nil 252 }) 253 254 queryIter.Finish(err) 255 } 256 257 // ReadOnly returns whether the database is read only. 258 func (fst *FSTree) ReadOnly() bool { 259 return false 260 } 261 262 // Injected returns whether the database is injected. 263 func (fst *FSTree) Injected() bool { 264 return false 265 } 266 267 // MaintainRecordStates maintains records states in the database. 268 func (fst *FSTree) MaintainRecordStates(ctx context.Context, purgeDeletedBefore time.Time, shadowDelete bool) error { 269 // TODO: implement MaintainRecordStates 270 return nil 271 } 272 273 // Shutdown shuts down the database. 274 func (fst *FSTree) Shutdown() error { 275 return nil 276 } 277 278 // writeFile mirrors os.WriteFile, replacing an existing file with the same 279 // name atomically. This is not atomic on Windows, but still an improvement. 280 // TODO: Replace with github.com/google/renamio.WriteFile as soon as it is fixed on Windows. 281 // TODO: This has become a wont-fix. Explore other options. 282 // This function is forked from https://github.com/google/renameio/blob/a368f9987532a68a3d676566141654a81aa8100b/writefile.go. 283 func writeFile(filename string, data []byte, perm os.FileMode) error { 284 t, err := renameio.TempFile("", filename) 285 if err != nil { 286 return err 287 } 288 defer t.Cleanup() //nolint:errcheck 289 290 // Set permissions before writing data, in case the data is sensitive. 291 if !onWindows { 292 if err := t.Chmod(perm); err != nil { 293 return err 294 } 295 } 296 297 if _, err := t.Write(data); err != nil { 298 return err 299 } 300 301 return t.CloseAtomicallyReplace() 302 }