mirror of
https://github.com/safing/portmaster
synced 2025-04-08 04:59:10 +00:00
302 lines
7.6 KiB
Go
302 lines
7.6 KiB
Go
/*
|
|
Package fstree provides a dead simple file-based database storage backend.
|
|
It is primarily meant for easy testing or storing big files that can easily be accesses directly, without datastore.
|
|
*/
|
|
package fstree
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/safing/portmaster/base/database/iterator"
|
|
"github.com/safing/portmaster/base/database/query"
|
|
"github.com/safing/portmaster/base/database/record"
|
|
"github.com/safing/portmaster/base/database/storage"
|
|
"github.com/safing/portmaster/base/utils/renameio"
|
|
)
|
|
|
|
const (
|
|
defaultFileMode = os.FileMode(0o0644)
|
|
defaultDirMode = os.FileMode(0o0755)
|
|
onWindows = runtime.GOOS == "windows"
|
|
)
|
|
|
|
// FSTree database storage.
|
|
type FSTree struct {
|
|
name string
|
|
basePath string
|
|
}
|
|
|
|
func init() {
|
|
_ = storage.Register("fstree", NewFSTree)
|
|
}
|
|
|
|
// NewFSTree returns a (new) FSTree database.
|
|
func NewFSTree(name, location string) (storage.Interface, error) {
|
|
basePath, err := filepath.Abs(location)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fstree: failed to validate path %s: %w", location, err)
|
|
}
|
|
|
|
file, err := os.Stat(basePath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
err = os.MkdirAll(basePath, defaultDirMode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fstree: failed to create directory %s: %w", basePath, err)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("fstree: failed to stat path %s: %w", basePath, err)
|
|
}
|
|
} else {
|
|
if !file.IsDir() {
|
|
return nil, fmt.Errorf("fstree: provided database path (%s) is a file", basePath)
|
|
}
|
|
}
|
|
|
|
return &FSTree{
|
|
name: name,
|
|
basePath: basePath,
|
|
}, nil
|
|
}
|
|
|
|
func (fst *FSTree) buildFilePath(key string, checkKeyLength bool) (string, error) {
|
|
// check key length
|
|
if checkKeyLength && len(key) < 1 {
|
|
return "", fmt.Errorf("fstree: key too short: %s", key)
|
|
}
|
|
// build filepath
|
|
dstPath := filepath.Join(fst.basePath, key) // Join also calls Clean()
|
|
if !strings.HasPrefix(dstPath, fst.basePath) {
|
|
return "", fmt.Errorf("fstree: key integrity check failed, compiled path is %s", dstPath)
|
|
}
|
|
// return
|
|
return dstPath, nil
|
|
}
|
|
|
|
// Get returns a database record.
|
|
func (fst *FSTree) Get(key string) (record.Record, error) {
|
|
dstPath, err := fst.buildFilePath(key, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := os.ReadFile(dstPath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, storage.ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("fstree: failed to read file %s: %w", dstPath, err)
|
|
}
|
|
|
|
r, err := record.NewRawWrapper(fst.name, key, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// GetMeta returns the metadata of a database record.
|
|
func (fst *FSTree) GetMeta(key string) (*record.Meta, error) {
|
|
// TODO: Replace with more performant variant.
|
|
|
|
r, err := fst.Get(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return r.Meta(), nil
|
|
}
|
|
|
|
// Put stores a record in the database.
|
|
func (fst *FSTree) Put(r record.Record) (record.Record, error) {
|
|
dstPath, err := fst.buildFilePath(r.DatabaseKey(), true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := r.MarshalRecord(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = writeFile(dstPath, data, defaultFileMode)
|
|
if err != nil {
|
|
// create dir and try again
|
|
err = os.MkdirAll(filepath.Dir(dstPath), defaultDirMode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fstree: failed to create directory %s: %w", filepath.Dir(dstPath), err)
|
|
}
|
|
err = writeFile(dstPath, data, defaultFileMode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fstree: could not write file %s: %w", dstPath, err)
|
|
}
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// Delete deletes a record from the database.
|
|
func (fst *FSTree) Delete(key string) error {
|
|
dstPath, err := fst.buildFilePath(key, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// remove entry
|
|
err = os.Remove(dstPath)
|
|
if err != nil {
|
|
return fmt.Errorf("fstree: could not delete %s: %w", dstPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Query returns a an iterator for the supplied query.
|
|
func (fst *FSTree) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) {
|
|
_, err := q.Check()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid query: %w", err)
|
|
}
|
|
|
|
walkPrefix, err := fst.buildFilePath(q.DatabaseKeyPrefix(), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileInfo, err := os.Stat(walkPrefix)
|
|
var walkRoot string
|
|
switch {
|
|
case err == nil && fileInfo.IsDir():
|
|
walkRoot = walkPrefix
|
|
case err == nil:
|
|
walkRoot = filepath.Dir(walkPrefix)
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
walkRoot = filepath.Dir(walkPrefix)
|
|
default: // err != nil
|
|
return nil, fmt.Errorf("fstree: could not stat query root %s: %w", walkPrefix, err)
|
|
}
|
|
|
|
queryIter := iterator.New()
|
|
|
|
go fst.queryExecutor(walkRoot, queryIter, q, local, internal)
|
|
return queryIter, nil
|
|
}
|
|
|
|
func (fst *FSTree) queryExecutor(walkRoot string, queryIter *iterator.Iterator, q *query.Query, local, internal bool) {
|
|
err := filepath.Walk(walkRoot, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("fstree: error in walking fs: %w", err)
|
|
}
|
|
|
|
if info.IsDir() {
|
|
// skip dir if not in scope
|
|
if !strings.HasPrefix(path, fst.basePath) {
|
|
return filepath.SkipDir
|
|
}
|
|
// continue
|
|
return nil
|
|
}
|
|
|
|
// still in scope?
|
|
if !strings.HasPrefix(path, fst.basePath) {
|
|
return nil
|
|
}
|
|
|
|
// read file
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("fstree: failed to read file %s: %w", path, err)
|
|
}
|
|
|
|
// parse
|
|
key, err := filepath.Rel(fst.basePath, path)
|
|
if err != nil {
|
|
return fmt.Errorf("fstree: failed to extract key from filepath %s: %w", path, err)
|
|
}
|
|
r, err := record.NewRawWrapper(fst.name, key, data)
|
|
if err != nil {
|
|
return fmt.Errorf("fstree: failed to load file %s: %w", path, err)
|
|
}
|
|
|
|
if !r.Meta().CheckValidity() {
|
|
// record is not valid
|
|
return nil
|
|
}
|
|
|
|
if !r.Meta().CheckPermission(local, internal) {
|
|
// no permission to access
|
|
return nil
|
|
}
|
|
|
|
// check if matches, then send
|
|
if q.MatchesRecord(r) {
|
|
select {
|
|
case queryIter.Next <- r:
|
|
case <-queryIter.Done:
|
|
case <-time.After(1 * time.Second):
|
|
return errors.New("fstree: query buffer full, timeout")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
queryIter.Finish(err)
|
|
}
|
|
|
|
// ReadOnly returns whether the database is read only.
|
|
func (fst *FSTree) ReadOnly() bool {
|
|
return false
|
|
}
|
|
|
|
// Injected returns whether the database is injected.
|
|
func (fst *FSTree) Injected() bool {
|
|
return false
|
|
}
|
|
|
|
// MaintainRecordStates maintains records states in the database.
|
|
func (fst *FSTree) MaintainRecordStates(ctx context.Context, purgeDeletedBefore time.Time, shadowDelete bool) error {
|
|
// TODO: implement MaintainRecordStates
|
|
return nil
|
|
}
|
|
|
|
// Shutdown shuts down the database.
|
|
func (fst *FSTree) Shutdown() error {
|
|
return nil
|
|
}
|
|
|
|
// writeFile mirrors os.WriteFile, replacing an existing file with the same
|
|
// name atomically. This is not atomic on Windows, but still an improvement.
|
|
// TODO: Replace with github.com/google/renamio.WriteFile as soon as it is fixed on Windows.
|
|
// TODO: This has become a wont-fix. Explore other options.
|
|
// This function is forked from https://github.com/google/renameio/blob/a368f9987532a68a3d676566141654a81aa8100b/writefile.go.
|
|
func writeFile(filename string, data []byte, perm os.FileMode) error {
|
|
t, err := renameio.TempFile("", filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer t.Cleanup() //nolint:errcheck
|
|
|
|
// Set permissions before writing data, in case the data is sensitive.
|
|
// TODO(vladimir): to set permissions on windows we need the full path of the file.
|
|
err = t.Chmod(perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := t.Write(data); err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.CloseAtomicallyReplace()
|
|
}
|