Files
restic/internal/fuse/snapshots_dirstruct.go
T
Ricardo Sawir 55e335ec6c fuse: reset treeCache on snapshot reload to fix stale latest symlink (#21873)
The treeCache in SnapshotsDir was never cleared when snapshots were
reloaded. This caused the "latest" symlink to keep pointing to the
previous snapshot even after new snapshots were added.

Add a generation counter to SnapshotsDirStructure that is incremented
whenever the directory structure is rebuilt (in makeDirs). The
treeCache checks this generation on each lookup and resets itself
when the generation changes, ensuring cached nodes (including symlinks)
are refreshed after a snapshot reload.
2026-06-22 18:31:51 +00:00

356 lines
8.2 KiB
Go

//go:build darwin || freebsd || linux
package fuse
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"path"
"sort"
"strings"
"sync"
"time"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/restic"
)
type MetaDirData struct {
// set if this is a symlink or a snapshot mount point
linkTarget string
snapshot *data.Snapshot
// names is set if this is a pseudo directory
names map[string]*MetaDirData
}
// SnapshotsDirStructure contains the directory structure for snapshots.
// It uses a paths and time template to generate a map of pathnames
// pointing to the actual snapshots. For templates that end with a time,
// also "latest" links are generated.
type SnapshotsDirStructure struct {
root *Root
pathTemplates []string
timeTemplate string
mutex sync.Mutex
// "" is the root path, subdirectory paths are assembled as parent+"/"+childFn
// thus all subdirectories are prefixed with a slash as the root is ""
// that way we don't need path processing special cases when using the entries tree
entries map[string]*MetaDirData
hash [sha256.Size]byte // Hash at last check.
lastCheck time.Time
// generation is incremented whenever the directory structure is rebuilt.
// It allows treeCache instances to detect stale entries and reset themselves.
generation int64
}
// NewSnapshotsDirStructure returns a new directory structure for snapshots.
func NewSnapshotsDirStructure(root *Root, pathTemplates []string, timeTemplate string) *SnapshotsDirStructure {
return &SnapshotsDirStructure{
root: root,
pathTemplates: pathTemplates,
timeTemplate: timeTemplate,
}
}
// pathsFromSn generates the paths from pathTemplate and timeTemplate
// where the variables are replaced by the snapshot data.
// The time is given as suffix if the pathTemplate ends with "%T".
func pathsFromSn(pathTemplate string, timeTemplate string, sn *data.Snapshot) (paths []string, timeSuffix string) {
timeformat := sn.Time.Format(timeTemplate)
inVerb := false
writeTime := false
out := make([]strings.Builder, 1)
for _, c := range pathTemplate {
if writeTime {
for i := range out {
out[i].WriteString(timeformat)
}
writeTime = false
}
if !inVerb {
if c == '%' {
inVerb = true
} else {
for i := range out {
out[i].WriteRune(c)
}
}
continue
}
var repl string
inVerb = false
switch c {
case 'T':
// lazy write; time might be returned as suffix
writeTime = true
continue
case 't':
if len(sn.Tags) == 0 {
return nil, ""
}
if len(sn.Tags) != 1 {
// needs special treatment: Rebuild the string builders
newout := make([]strings.Builder, len(out)*len(sn.Tags))
for i, tag := range sn.Tags {
tag = filenameFromTag(tag)
for j := range out {
newout[i*len(out)+j].WriteString(out[j].String() + tag)
}
}
out = newout
continue
}
repl = sn.Tags[0]
case 'i':
repl = sn.ID().Str()
case 'I':
repl = sn.ID().String()
case 'u':
repl = sn.Username
case 'h':
repl = sn.Hostname
default:
repl = string(c)
}
// write replacement string to all string builders
for i := range out {
out[i].WriteString(repl)
}
}
for i := range out {
paths = append(paths, out[i].String())
}
if writeTime {
timeSuffix = timeformat
}
return paths, timeSuffix
}
// Some tags are problematic when used as filenames:
//
// ""
// ".", ".."
// anything containing '/'
//
// Replace all special character by underscores "_", an empty tag is also represented as a underscore.
func filenameFromTag(tag string) string {
switch tag {
case "", ".":
return "_"
case "..":
return "__"
}
return strings.ReplaceAll(tag, "/", "_")
}
// determine static path prefix
func staticPrefix(pathTemplate string) (prefix string) {
inVerb := false
patternStart := -1
outer:
for i, c := range pathTemplate {
if !inVerb {
if c == '%' {
inVerb = true
}
continue
}
inVerb = false
switch c {
case 'i', 'I', 'u', 'h', 't', 'T':
patternStart = i
break outer
}
}
if patternStart < 0 {
// ignore patterns without template variable
return ""
}
p := pathTemplate[:patternStart]
idx := strings.LastIndex(p, "/")
if idx < 0 {
return ""
}
return p[:idx]
}
// uniqueName returns a unique name to be used for prefix+name.
// It appends -number to make the name unique.
func uniqueName(entries map[string]*MetaDirData, prefix, name string) string {
newname := name
for i := 1; ; i++ {
if _, ok := entries[prefix+newname]; !ok {
break
}
newname = fmt.Sprintf("%s-%d", name, i)
}
return newname
}
// makeDirs inserts all paths generated from pathTemplates and
// TimeTemplate for all given snapshots into d.names.
// Also adds d.latest links if "%T" is at end of a path template
func (d *SnapshotsDirStructure) makeDirs(snapshots data.Snapshots) {
entries := make(map[string]*MetaDirData)
type mountData struct {
sn *data.Snapshot
linkTarget string // if linkTarget!= "", this is a symlink
childFn string
child *MetaDirData
}
// recursively build tree structure
var mount func(path string, data mountData)
mount = func(path string, data mountData) {
e := entries[path]
if e == nil {
e = &MetaDirData{}
}
if data.sn != nil {
e.snapshot = data.sn
e.linkTarget = data.linkTarget
} else {
// intermediate directory, register as a child directory
if e.names == nil {
e.names = make(map[string]*MetaDirData)
}
if data.child != nil {
e.names[data.childFn] = data.child
}
}
entries[path] = e
slashIdx := strings.LastIndex(path, "/")
if slashIdx >= 0 {
// add to parent dir, but without snapshot
mount(path[:slashIdx], mountData{childFn: path[slashIdx+1:], child: e})
}
}
// root directory
mount("", mountData{})
// insert pure directories; needed to get empty structure even if there
// are no snapshots in these dirs
for _, p := range d.pathTemplates {
p = staticPrefix(p)
if p != "" {
mount(path.Clean("/"+p), mountData{})
}
}
latestTime := make(map[string]time.Time)
for _, sn := range snapshots {
for _, templ := range d.pathTemplates {
paths, timeSuffix := pathsFromSn(templ, d.timeTemplate, sn)
for _, p := range paths {
if p != "" {
p = "/" + p
}
suffix := uniqueName(entries, p, timeSuffix)
mount(path.Clean(p+suffix), mountData{sn: sn})
if timeSuffix != "" {
lt, ok := latestTime[p]
if !ok || !sn.Time.Before(lt) {
debug.Log("link (update) %v -> %v\n", p, suffix)
// inject symlink
mount(path.Clean(p+"/latest"), mountData{sn: sn, linkTarget: suffix})
latestTime[p] = sn.Time
}
}
}
}
}
d.entries = entries
d.generation++
}
const minSnapshotsReloadTime = 60 * time.Second
// update snapshots if repository has changed
func (d *SnapshotsDirStructure) updateSnapshots(ctx context.Context) error {
d.mutex.Lock()
defer d.mutex.Unlock()
if time.Since(d.lastCheck) < minSnapshotsReloadTime {
return nil
}
var snapshots data.Snapshots
err := d.root.cfg.Filter.FindAll(ctx, d.root.repo, d.root.repo, nil, func(_ string, sn *data.Snapshot, _ error) error {
if sn != nil {
snapshots = append(snapshots, sn)
}
return nil
})
if err != nil {
return err
}
// Sort snapshots ascending by time, using the id to break ties.
// This needs to be done before hashing.
sort.Slice(snapshots, func(i, j int) bool {
si, sj := snapshots[i], snapshots[j]
if si.Time.Equal(sj.Time) {
return bytes.Compare(si.ID()[:], sj.ID()[:]) < 0
}
return si.Time.Before(sj.Time)
})
// We update the snapshots when the hash of their id's changes.
h := sha256.New()
for _, sn := range snapshots {
h.Write(sn.ID()[:])
}
var hash [sha256.Size]byte
h.Sum(hash[:0])
if d.hash == hash {
d.lastCheck = time.Now()
return nil
}
err = d.root.repo.LoadIndex(ctx, restic.NoopTerminalCounterFactory)
if err != nil {
return err
}
d.lastCheck = time.Now()
d.hash = hash
d.makeDirs(snapshots)
return nil
}
func (d *SnapshotsDirStructure) UpdatePrefix(ctx context.Context, prefix string) (*MetaDirData, int64, error) {
err := d.updateSnapshots(ctx)
if err != nil {
return nil, 0, err
}
d.mutex.Lock()
defer d.mutex.Unlock()
return d.entries[prefix], d.generation, nil
}