681 lines
18 KiB
Go
681 lines
18 KiB
Go
package state
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.clan.lol/clan/data-mesher/pkg/config"
|
|
"git.clan.lol/clan/data-mesher/pkg/crypto"
|
|
"git.clan.lol/clan/data-mesher/pkg/model"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
const (
|
|
DirTemp = "temp"
|
|
DirFiles = "files"
|
|
|
|
// MaxClockSkew is the maximum amount a signature's SignedAt may exceed the current wall-clock time.
|
|
// This prevents an authorized signer from pinning a file with a far-future timestamp that honest signers can
|
|
// never supersede.
|
|
MaxClockSkew = 5 * time.Minute
|
|
)
|
|
|
|
var (
|
|
ErrFileNotFound = errors.New("file not found")
|
|
ErrTombstoneFileExists = errors.New("tombstone file exists")
|
|
ErrSignedAtFuture = errors.New("signature timestamp too far in the future")
|
|
)
|
|
|
|
type Files struct {
|
|
cfg *config.Config
|
|
tmpDir string
|
|
filesDir string
|
|
signatures *Signatures
|
|
}
|
|
|
|
func NewFiles(cfg *config.Config) (*Files, error) {
|
|
// ensure the files directory exists
|
|
filesDir := path.Join(cfg.StateDirectory, DirFiles)
|
|
if err := os.MkdirAll(filesDir, 0o750); err != nil {
|
|
return nil, fmt.Errorf("failed to create the files directory: %w", err)
|
|
}
|
|
|
|
// clean up any partial downloads from previous runs by recreating the temp directory
|
|
tmpDir := path.Join(cfg.StateDirectory, DirTemp)
|
|
|
|
if err := recreateTempDir(tmpDir); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create a new signatures store
|
|
signatures, err := NewSignatures(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create signatures store: %w", err)
|
|
}
|
|
|
|
// create the file store
|
|
f := &Files{
|
|
cfg: cfg,
|
|
filesDir: filesDir,
|
|
tmpDir: tmpDir,
|
|
signatures: signatures,
|
|
}
|
|
|
|
// ensure top-level network directories exist for all configured networks
|
|
for _, network := range cfg.NetworksByID {
|
|
dirPath := f.NetworkPath(network)
|
|
|
|
if err = os.MkdirAll(dirPath, 0o750); err != nil {
|
|
return nil, fmt.Errorf("failed to create network directory: %w", err)
|
|
}
|
|
|
|
// create a symlink based on the network's name
|
|
symPath := path.Join(f.filesDir, network.Name)
|
|
|
|
// remove the existing symlink, if it exists
|
|
err = os.Remove(symPath)
|
|
if !os.IsNotExist(err) && err != nil {
|
|
return nil, fmt.Errorf("failed to remove existing symlink: %w", err)
|
|
}
|
|
|
|
// create the symlink
|
|
if err = os.Symlink(dirPath, symPath); err != nil {
|
|
return nil, fmt.Errorf("failed to create symlink: %w", err)
|
|
}
|
|
|
|
// create plugin subdirectories
|
|
for _, dir := range cfg.PluginDirectories {
|
|
pluginPath := path.Join(dirPath, dir)
|
|
if err = os.MkdirAll(pluginPath, 0o750); err != nil {
|
|
return nil, fmt.Errorf("failed to create plugin directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// perform an integrity check before returning
|
|
if err = checkIntegrity(f); err != nil {
|
|
return nil, fmt.Errorf("integrity check failed: %w", err)
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
func (f *Files) BeginTx(writable bool) (*bolt.Tx, error) {
|
|
return f.signatures.BeginTx(writable)
|
|
}
|
|
|
|
func (f *Files) NetworkPath(network *config.Network) string {
|
|
return path.Join(f.filesDir, network.ID.URLEncoded())
|
|
}
|
|
|
|
// FilePath returns the on-disk path for a file, partitioned by network ID.
|
|
// It returns an error if the resolved path escapes the network directory.
|
|
func (f *Files) FilePath(network *config.Network, name string) (string, error) {
|
|
base, err := filepath.Abs(f.NetworkPath(network))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to resolve network path: %w", err)
|
|
}
|
|
|
|
clean := filepath.Clean(filepath.Join(base, name))
|
|
|
|
if !strings.HasPrefix(clean, path.Join(base)) && clean != base {
|
|
return "", fmt.Errorf("path traversal detected: name %q resolves outside network directory", name)
|
|
}
|
|
|
|
return clean, nil
|
|
}
|
|
|
|
func (f *Files) ListSignatures(tx *bolt.Tx) (SignatureReader, error) {
|
|
r, err := f.signatures.List(tx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list signatures: %w", err)
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (f *Files) Get(networkID *crypto.PublicKey, name string) (io.Reader, *model.Signature, error) {
|
|
// look up network config
|
|
network, ok := f.cfg.NetworksByID[networkID.String()]
|
|
if !ok {
|
|
return nil, nil, ErrNetworkNotConfigured
|
|
}
|
|
|
|
// start a read-only transaction
|
|
tx, err := f.signatures.BeginTx(false)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
|
|
// ensure it gets released when we're done
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
// check for a signature first
|
|
var signature model.Signature
|
|
|
|
err = f.signatures.Get(tx, networkID, name, &signature)
|
|
|
|
// we treat a signature not found as a file not found
|
|
// tombstones are also treated as file not found
|
|
if errors.Is(err, ErrSignatureNotFound) || errors.Is(err, ErrNetworkNotFound) ||
|
|
signature.Type == model.SignatureTypeTombstone {
|
|
return nil, nil, ErrFileNotFound
|
|
} else if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get signature: %w", err)
|
|
}
|
|
|
|
// expired files are also treated as file not found. The on-disk file may
|
|
// still exist briefly until the sweeper runs; we hide it from readers
|
|
// immediately so behaviour is consistent across the cluster.
|
|
if signature.Expired(f.cfg.Now()) {
|
|
return nil, nil, ErrFileNotFound
|
|
}
|
|
|
|
// try to open a reader for the file
|
|
filePath, err := f.FilePath(network, name)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
reader, err := os.Open(filePath) //nolint:gosec // filePath is constructed from validated network dir and name
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// todo we are in a corrupt state, log it, maybe clean up the signature entry?
|
|
return nil, nil, ErrFileNotFound
|
|
} else if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
|
|
return reader, &signature, nil
|
|
}
|
|
|
|
// GetSignature retrieves the signature for a file, including tombstones.
|
|
// Use this when you need to compare timestamps including deleted files.
|
|
func (f *Files) GetSignature(network *crypto.PublicKey, name string, sig *model.Signature) error {
|
|
// start a read-only transaction
|
|
tx, err := f.signatures.db.Begin(false)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
|
|
// ensure it gets released when we're done
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
return f.signatures.Get(tx, network, name, sig)
|
|
}
|
|
|
|
func (f *Files) Put(sig *model.Signature, r io.Reader) (bool, error) {
|
|
// look up network config
|
|
network, ok := f.cfg.NetworksByID[sig.NetworkID.String()]
|
|
if !ok {
|
|
return false, fmt.Errorf("network %s is not configured", sig.NetworkID)
|
|
}
|
|
|
|
// validate the file name to prevent path traversal
|
|
if err := model.ValidateName(sig.Name); err != nil {
|
|
return false, fmt.Errorf("rejecting file with invalid name: %w", err)
|
|
}
|
|
|
|
// reject signatures with timestamps too far in the future
|
|
if sig.SignedAt.After(time.Now().Add(MaxClockSkew)) {
|
|
return false, fmt.Errorf("%w: %s", ErrSignedAtFuture, sig.SignedAt)
|
|
}
|
|
|
|
// tombstones should use PutTombstone instead
|
|
if sig.Type != model.SignatureTypeFile {
|
|
return f.PutTombstone(sig)
|
|
}
|
|
|
|
// create a writer for the signature
|
|
sw := model.SignatureWriter{
|
|
Type: sig.Type,
|
|
NetworkID: sig.NetworkID,
|
|
Name: sig.Name,
|
|
SignedAt: sig.SignedAt,
|
|
ValidFor: sig.ValidFor,
|
|
}
|
|
|
|
// create a temp file for the content
|
|
// we replace any path separators to keep the temp directory flat
|
|
tempName := path.Join(
|
|
f.tmpDir,
|
|
fmt.Sprintf(
|
|
"%s.%d",
|
|
strings.ReplaceAll(sig.Name, "/", "_"),
|
|
sig.SignedAt.UnixNano(),
|
|
),
|
|
)
|
|
|
|
//nolint:gosec // tempName is constructed from validated filesDir and sig.Name
|
|
file, err := os.OpenFile(tempName, os.O_CREATE|os.O_WRONLY, 0o640)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to create temporary file: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
// ensure the file was closed when we're done
|
|
_ = file.Close()
|
|
|
|
// check if the temp file still exists (failed put)
|
|
_, err := os.Stat(tempName)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return
|
|
}
|
|
|
|
// if it does, remove it
|
|
if err = os.Remove(tempName); err != nil {
|
|
slog.Error("failed to remove temporary file", slog.String("name", tempName), slog.Any("err", err))
|
|
}
|
|
}()
|
|
|
|
// create a tee reader so we can verify the signature and write to the temp file at the same time
|
|
tr := io.TeeReader(r, &sw)
|
|
|
|
// copy to the temporary file
|
|
if _, err := io.Copy(file, tr); err != nil {
|
|
return false, fmt.Errorf("failed to copy content to temp file: %w", err)
|
|
}
|
|
|
|
if err = file.Close(); err != nil {
|
|
return false, fmt.Errorf("failed to close temp file: %w", err)
|
|
}
|
|
|
|
// Set the file's mtime to match the signature timestamp.
|
|
// This will make it easy for plugins to determine how old a file is without having to interact with the
|
|
// signature database.
|
|
if err = os.Chtimes(tempName, sig.SignedAt, sig.SignedAt); err != nil {
|
|
return false, fmt.Errorf("failed to set temp file mtime: %w", err)
|
|
}
|
|
|
|
// change permissions to readonly
|
|
//nolint:gosec
|
|
if err = os.Chmod(tempName, 0o644); err != nil {
|
|
return false, fmt.Errorf("failed to set temp file permissions: %w", err)
|
|
}
|
|
|
|
// check the signature
|
|
if err = sw.Close(); err != nil {
|
|
return false, fmt.Errorf("failed to close signature writer: %w", err)
|
|
}
|
|
|
|
// verify actual size matches declared size for file signatures
|
|
if sig.FileSize > 0 && sw.Size() != sig.FileSize {
|
|
return false, fmt.Errorf(
|
|
"%w: file size mismatch (got %d, expected %d)",
|
|
model.ErrSignatureNotValid, sw.Size(), sig.FileSize,
|
|
)
|
|
}
|
|
|
|
valid, err := sw.Verify(sig.SignedBy, sig.Signature, sig.FileSize)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to verify signature: %w", err)
|
|
}
|
|
|
|
if !valid {
|
|
return false, model.ErrSignatureNotValid
|
|
}
|
|
|
|
var updated bool
|
|
|
|
// start a write transaction
|
|
tx, err := f.signatures.BeginTx(true)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
|
|
// ensure it gets released when we're done
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
// try to write the signature first
|
|
updated, err = f.signatures.PutIfLater(tx, sig)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to put signature: %w", err)
|
|
}
|
|
|
|
// if the signature was not updated, we can skip moving the file
|
|
if !updated {
|
|
return false, nil
|
|
}
|
|
|
|
// otherwise, move the temp file to the final location in the files directory
|
|
finalPath, err := f.FilePath(network, sig.Name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// ensure any subdirectories are created since sig.Name can contain path separators
|
|
dir := path.Dir(finalPath)
|
|
if err = os.MkdirAll(dir, 0o750); err != nil {
|
|
return true, fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
if err = os.Rename(tempName, finalPath); err != nil {
|
|
return false, fmt.Errorf("failed to rename temporary file: %w", err)
|
|
}
|
|
|
|
// commit the transaction
|
|
if err = tx.Commit(); err != nil {
|
|
return false, fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// PutTombstone stores a tombstone (deletion record) and removes the file from disk if it exists.
|
|
func (f *Files) PutTombstone(sig *model.Signature) (bool, error) {
|
|
// look up network config
|
|
network, ok := f.cfg.NetworksByID[sig.NetworkID.String()]
|
|
if !ok {
|
|
return false, fmt.Errorf("network %s is not configured", sig.NetworkID)
|
|
}
|
|
|
|
// validate the file name to prevent path traversal
|
|
if err := model.ValidateName(sig.Name); err != nil {
|
|
return false, fmt.Errorf("rejecting tombstone with invalid name: %w", err)
|
|
}
|
|
|
|
// reject signatures with timestamps too far in the future
|
|
if sig.SignedAt.After(time.Now().Add(MaxClockSkew)) {
|
|
return false, fmt.Errorf("%w: %s", ErrSignedAtFuture, sig.SignedAt)
|
|
}
|
|
|
|
if sig.Type != model.SignatureTypeTombstone {
|
|
return false, errors.New("signature is not a tombstone")
|
|
}
|
|
|
|
// verify the tombstone signature
|
|
if err := sig.Verify(nil); err != nil {
|
|
return false, fmt.Errorf("tombstone verification failed: %w", err)
|
|
}
|
|
|
|
// start a new transaction
|
|
tx, err := f.signatures.BeginTx(true)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
|
|
// ensure it gets released when we're done
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
var updated bool
|
|
|
|
updated, err = f.signatures.PutIfLater(tx, sig)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to put tombstone: %w", err)
|
|
}
|
|
|
|
// if the tombstone was not written (older than existing), skip file deletion
|
|
if !updated {
|
|
return updated, nil
|
|
}
|
|
|
|
// otherwise remove the file from disk, if it exists
|
|
filePath, err := f.FilePath(network, sig.Name)
|
|
if err != nil {
|
|
return updated, err
|
|
}
|
|
|
|
if fileErr := os.Remove(filePath); fileErr != nil && !errors.Is(fileErr, os.ErrNotExist) {
|
|
return updated, fmt.Errorf("failed to delete file: %w", fileErr)
|
|
}
|
|
|
|
// commit the transaction
|
|
if err = tx.Commit(); err != nil {
|
|
return updated, fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return updated, nil
|
|
}
|
|
|
|
func (f *Files) Delete(networkID *crypto.PublicKey, name string) error {
|
|
// look up network config
|
|
network, ok := f.cfg.NetworksByID[networkID.String()]
|
|
if !ok {
|
|
return fmt.Errorf("network %s is not configured", networkID)
|
|
}
|
|
|
|
// start a transaction
|
|
tx, err := f.signatures.BeginTx(true)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
|
|
// ensure it gets released when we're done
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
// attempt to delete the signature
|
|
if err = f.signatures.Delete(tx, networkID, name); err != nil {
|
|
return fmt.Errorf("failed to delete signature: %w", err)
|
|
}
|
|
|
|
// remove the file, ignoring if it doesn't exist
|
|
delPath, err := f.FilePath(network, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = os.Remove(delPath); !os.IsNotExist(err) && err != nil {
|
|
return fmt.Errorf("failed to delete file: %w", err)
|
|
}
|
|
|
|
// commit the transaction
|
|
if err = tx.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *Files) Close() error {
|
|
return f.signatures.Close()
|
|
}
|
|
|
|
// StateDir returns the state directory path for testing purposes.
|
|
func (f *Files) StateDir() string {
|
|
return f.cfg.StateDirectory
|
|
}
|
|
|
|
func recreateTempDir(path string) error {
|
|
// remove the tmp directory, it will be recreated on startup
|
|
err := os.RemoveAll(path)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("failed to remove temp directory: %w", err)
|
|
}
|
|
|
|
// recreate the temp directory
|
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
|
return fmt.Errorf("failed to create the temp directory: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkIntegrity(f *Files) error {
|
|
slog.Info("checking file integrity")
|
|
|
|
// start a transaction
|
|
tx, err := f.signatures.BeginTx(true)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin read transaction: %w", err)
|
|
}
|
|
|
|
// ensure it gets released when we're done
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
// create a signatures reader
|
|
r, err := f.ListSignatures(tx)
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
|
|
return fmt.Errorf("failed to list signatures: %w", err)
|
|
}
|
|
|
|
buf := make([]model.Signature, 16)
|
|
|
|
for {
|
|
n, readErr := r.Read(buf)
|
|
|
|
for _, sig := range buf[:n] {
|
|
if repairErr := f.repairSignature(tx, &sig); repairErr != nil {
|
|
return repairErr
|
|
}
|
|
}
|
|
|
|
if errors.Is(readErr, io.EOF) {
|
|
break
|
|
} else if readErr != nil {
|
|
return fmt.Errorf("failed to read signatures: %w", readErr)
|
|
}
|
|
}
|
|
|
|
// commit the transaction, as we want to complete the integrity check even if some signatures failed to verify }
|
|
if err = tx.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
slog.Info("file integrity check complete")
|
|
|
|
return nil
|
|
}
|
|
|
|
// repairSignature verifies a single signature and takes remedial action if it is invalid.
|
|
func (f *Files) repairSignature(tx *bolt.Tx, sig *model.Signature) error {
|
|
err := f.verifySignature(sig)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
switch {
|
|
case errors.Is(err, ErrFileNotFound):
|
|
slog.Warn("file no longer exists, removing", slog.String("file", sig.Name))
|
|
|
|
return f.removeSignature(tx, sig.Name)
|
|
|
|
case errors.Is(err, model.ErrSignatureNotValid):
|
|
slog.Warn("file is no longer valid, removing", slog.String("file", sig.Name))
|
|
|
|
return f.removeSignature(tx, sig.Name)
|
|
|
|
case errors.Is(err, ErrTombstoneFileExists):
|
|
slog.Warn("tombstoned file exists, removing", slog.String("file", sig.Name))
|
|
|
|
return f.removeFile(sig)
|
|
|
|
default:
|
|
return fmt.Errorf("failed to verify signature: %w", err)
|
|
}
|
|
}
|
|
|
|
func (f *Files) verifySignature(sig *model.Signature) error {
|
|
switch sig.Type {
|
|
case model.SignatureTypeTombstone:
|
|
return f.verifyTombstone(sig)
|
|
case model.SignatureTypeFile:
|
|
return f.verifyFile(sig)
|
|
default:
|
|
return fmt.Errorf("unknown signature type: %d", sig.Type)
|
|
}
|
|
}
|
|
|
|
func (f *Files) verifyTombstone(sig *model.Signature) error {
|
|
// look up network config
|
|
network, ok := f.cfg.NetworksByID[sig.NetworkID.String()]
|
|
if !ok {
|
|
return fmt.Errorf("network %s is not configured", sig.NetworkID)
|
|
}
|
|
|
|
if sig.Type != model.SignatureTypeTombstone {
|
|
return errors.New("signature is not a tombstone")
|
|
}
|
|
|
|
// verify there is no file on disk
|
|
tsPath, err := f.FilePath(network, sig.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = os.Stat(tsPath)
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("%w: %s", ErrTombstoneFileExists, sig.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *Files) verifyFile(sig *model.Signature) error {
|
|
// look up network config
|
|
network, ok := f.cfg.NetworksByID[sig.NetworkID.String()]
|
|
if !ok {
|
|
return fmt.Errorf("network %s is not configured", sig.NetworkID)
|
|
}
|
|
|
|
// try to open a reader for the file
|
|
vfPath, err := f.FilePath(network, sig.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err := os.Open(vfPath) //nolint:gosec // vfPath is constructed from validated network dir and name
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("%w: %s", ErrFileNotFound, sig.Name)
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
|
|
// verify it matches the signature
|
|
if err = sig.Verify(r); err != nil {
|
|
return fmt.Errorf("signature verification failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *Files) removeFile(sig *model.Signature) error {
|
|
// look up network config
|
|
network, ok := f.cfg.NetworksByID[sig.NetworkID.String()]
|
|
if !ok {
|
|
return fmt.Errorf("network %s is not configured", sig.NetworkID)
|
|
}
|
|
|
|
rmPath, err := f.FilePath(network, sig.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = os.Remove(rmPath)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("failed to remove tombstoned file %s: %w", sig.Name, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *Files) removeSignature(tx *bolt.Tx, name string) error {
|
|
if err := f.signatures.Delete(tx, f.cfg.Network.ID, name); err != nil {
|
|
return fmt.Errorf(
|
|
"failed to remove signature for network %s, name %s: %w",
|
|
f.cfg.Network.ID, name, err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|