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  }