mirror of
https://github.com/octoleo/restic.git
synced 2024-12-22 19:08:55 +00:00
352 lines
8.0 KiB
Go
352 lines
8.0 KiB
Go
//go:build darwin || freebsd || linux
|
|
// +build darwin freebsd linux
|
|
|
|
package fuse
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/restic/restic/internal/debug"
|
|
"github.com/restic/restic/internal/restic"
|
|
|
|
"github.com/minio/sha256-simd"
|
|
)
|
|
|
|
type MetaDirData struct {
|
|
// set if this is a symlink or a snapshot mount point
|
|
linkTarget string
|
|
snapshot *restic.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
|
|
}
|
|
|
|
// 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 *restic.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 restic.Snapshots) {
|
|
entries := make(map[string]*MetaDirData)
|
|
|
|
type mountData struct {
|
|
sn *restic.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
|
|
}
|
|
|
|
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 restic.Snapshots
|
|
err := d.root.cfg.Filter.FindAll(ctx, d.root.repo.Backend(), d.root.repo, nil, func(id string, sn *restic.Snapshot, err 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, nil)
|
|
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, error) {
|
|
err := d.updateSnapshots(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.entries[prefix], nil
|
|
}
|