// +build darwin freebsd linux package fuse import ( "context" "fmt" "os" "time" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/restic" "bazil.org/fuse" "bazil.org/fuse/fs" ) // SnapshotsDir is a fuse directory which contains snapshots named by timestamp. type SnapshotsDir struct { inode uint64 root *Root names map[string]*restic.Snapshot latest string tag string host string snCount int template string } // SnapshotsIDSDir is a fuse directory which contains snapshots named by ids. type SnapshotsIDSDir struct { inode uint64 root *Root names map[string]*restic.Snapshot snCount int } // HostsDir is a fuse directory which contains hosts. type HostsDir struct { inode uint64 root *Root hosts map[string]bool snCount int } // TagsDir is a fuse directory which contains tags. type TagsDir struct { inode uint64 root *Root tags map[string]bool snCount int } // SnapshotLink type snapshotLink struct { root *Root inode uint64 target string snapshot *restic.Snapshot } // ensure that *SnapshotsDir implements these interfaces var _ = fs.HandleReadDirAller(&SnapshotsDir{}) var _ = fs.NodeStringLookuper(&SnapshotsDir{}) var _ = fs.HandleReadDirAller(&SnapshotsIDSDir{}) var _ = fs.NodeStringLookuper(&SnapshotsIDSDir{}) var _ = fs.HandleReadDirAller(&TagsDir{}) var _ = fs.NodeStringLookuper(&TagsDir{}) var _ = fs.HandleReadDirAller(&HostsDir{}) var _ = fs.NodeStringLookuper(&HostsDir{}) var _ = fs.NodeReadlinker(&snapshotLink{}) // read tag names from the current repository-state. func updateTagNames(d *TagsDir) { if d.snCount != d.root.snCount { d.snCount = d.root.snCount d.tags = make(map[string]bool, len(d.root.snapshots)) for _, snapshot := range d.root.snapshots { for _, tag := range snapshot.Tags { if tag != "" { d.tags[tag] = true } } } } } // read host names from the current repository-state. func updateHostsNames(d *HostsDir) { if d.snCount != d.root.snCount { d.snCount = d.root.snCount d.hosts = make(map[string]bool, len(d.root.snapshots)) for _, snapshot := range d.root.snapshots { d.hosts[snapshot.Hostname] = true } } } // read snapshot id names from the current repository-state. func updateSnapshotIDSNames(d *SnapshotsIDSDir) { if d.snCount != d.root.snCount { d.snCount = d.root.snCount for _, sn := range d.root.snapshots { name := sn.ID().Str() d.names[name] = sn } } } // NewSnapshotsDir returns a new directory containing snapshots. func NewSnapshotsDir(root *Root, inode uint64, tag string, host string) *SnapshotsDir { debug.Log("create snapshots dir, inode %d", inode) d := &SnapshotsDir{ root: root, inode: inode, names: make(map[string]*restic.Snapshot), latest: "", tag: tag, host: host, template: root.cfg.SnapshotTemplate, } return d } // NewSnapshotsIDSDir returns a new directory containing snapshots named by ids. func NewSnapshotsIDSDir(root *Root, inode uint64) *SnapshotsIDSDir { debug.Log("create snapshots ids dir, inode %d", inode) d := &SnapshotsIDSDir{ root: root, inode: inode, names: make(map[string]*restic.Snapshot), } return d } // NewHostsDir returns a new directory containing host names func NewHostsDir(root *Root, inode uint64) *HostsDir { debug.Log("create hosts dir, inode %d", inode) d := &HostsDir{ root: root, inode: inode, hosts: make(map[string]bool), } return d } // NewTagsDir returns a new directory containing tag names func NewTagsDir(root *Root, inode uint64) *TagsDir { debug.Log("create tags dir, inode %d", inode) d := &TagsDir{ root: root, inode: inode, tags: make(map[string]bool), } return d } // Attr returns the attributes for the root node. func (d *SnapshotsDir) Attr(ctx context.Context, attr *fuse.Attr) error { attr.Inode = d.inode attr.Mode = os.ModeDir | 0555 attr.Uid = d.root.uid attr.Gid = d.root.gid debug.Log("attr: %v", attr) return nil } // Attr returns the attributes for the SnapshotsDir. func (d *SnapshotsIDSDir) Attr(ctx context.Context, attr *fuse.Attr) error { attr.Inode = d.inode attr.Mode = os.ModeDir | 0555 attr.Uid = d.root.uid attr.Gid = d.root.gid debug.Log("attr: %v", attr) return nil } // Attr returns the attributes for the HostsDir. func (d *HostsDir) Attr(ctx context.Context, attr *fuse.Attr) error { attr.Inode = d.inode attr.Mode = os.ModeDir | 0555 attr.Uid = d.root.uid attr.Gid = d.root.gid debug.Log("attr: %v", attr) return nil } // Attr returns the attributes for the TagsDir. func (d *TagsDir) Attr(ctx context.Context, attr *fuse.Attr) error { attr.Inode = d.inode attr.Mode = os.ModeDir | 0555 attr.Uid = d.root.uid attr.Gid = d.root.gid debug.Log("attr: %v", attr) return nil } // search element in string list. func isElem(e string, list []string) bool { for _, x := range list { if e == x { return true } } return false } const minSnapshotsReloadTime = 60 * time.Second // update snapshots if repository has changed func updateSnapshots(ctx context.Context, root *Root) error { if time.Since(root.lastCheck) < minSnapshotsReloadTime { return nil } snapshots, err := restic.FindFilteredSnapshots(ctx, root.repo, root.cfg.Hosts, root.cfg.Tags, root.cfg.Paths) if err != nil { return err } if root.snCount != len(snapshots) { root.snCount = len(snapshots) err := root.repo.LoadIndex(ctx) if err != nil { return err } root.snapshots = snapshots } root.lastCheck = time.Now() return nil } // read snapshot timestamps from the current repository-state. func updateSnapshotNames(d *SnapshotsDir, template string) { if d.snCount != d.root.snCount { d.snCount = d.root.snCount var latestTime time.Time d.latest = "" d.names = make(map[string]*restic.Snapshot, len(d.root.snapshots)) for _, sn := range d.root.snapshots { if d.tag == "" || isElem(d.tag, sn.Tags) { if d.host == "" || d.host == sn.Hostname { name := sn.Time.Format(template) if d.latest == "" || !sn.Time.Before(latestTime) { latestTime = sn.Time d.latest = name } for i := 1; ; i++ { if _, ok := d.names[name]; !ok { break } name = fmt.Sprintf("%s-%d", sn.Time.Format(template), i) } d.names[name] = sn } } } } } // ReadDirAll returns all entries of the SnapshotsDir. func (d *SnapshotsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { debug.Log("ReadDirAll()") // update snapshots err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update snapshot names updateSnapshotNames(d, d.root.cfg.SnapshotTemplate) items := []fuse.Dirent{ { Inode: d.inode, Name: ".", Type: fuse.DT_Dir, }, { Inode: d.root.inode, Name: "..", Type: fuse.DT_Dir, }, } for name := range d.names { items = append(items, fuse.Dirent{ Inode: fs.GenerateDynamicInode(d.inode, name), Name: name, Type: fuse.DT_Dir, }) } // Latest if d.latest != "" { items = append(items, fuse.Dirent{ Inode: fs.GenerateDynamicInode(d.inode, "latest"), Name: "latest", Type: fuse.DT_Link, }) } return items, nil } // ReadDirAll returns all entries of the SnapshotsIDSDir. func (d *SnapshotsIDSDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { debug.Log("ReadDirAll()") // update snapshots err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update snapshot ids updateSnapshotIDSNames(d) items := []fuse.Dirent{ { Inode: d.inode, Name: ".", Type: fuse.DT_Dir, }, { Inode: d.root.inode, Name: "..", Type: fuse.DT_Dir, }, } for name := range d.names { items = append(items, fuse.Dirent{ Inode: fs.GenerateDynamicInode(d.inode, name), Name: name, Type: fuse.DT_Dir, }) } return items, nil } // ReadDirAll returns all entries of the HostsDir. func (d *HostsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { debug.Log("ReadDirAll()") // update snapshots err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update host names updateHostsNames(d) items := []fuse.Dirent{ { Inode: d.inode, Name: ".", Type: fuse.DT_Dir, }, { Inode: d.root.inode, Name: "..", Type: fuse.DT_Dir, }, } for host := range d.hosts { items = append(items, fuse.Dirent{ Inode: fs.GenerateDynamicInode(d.inode, host), Name: host, Type: fuse.DT_Dir, }) } return items, nil } // ReadDirAll returns all entries of the TagsDir. func (d *TagsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { debug.Log("ReadDirAll()") // update snapshots err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update tag names updateTagNames(d) items := []fuse.Dirent{ { Inode: d.inode, Name: ".", Type: fuse.DT_Dir, }, { Inode: d.root.inode, Name: "..", Type: fuse.DT_Dir, }, } for tag := range d.tags { items = append(items, fuse.Dirent{ Inode: fs.GenerateDynamicInode(d.inode, tag), Name: tag, Type: fuse.DT_Dir, }) } return items, nil } // newSnapshotLink func newSnapshotLink(ctx context.Context, root *Root, inode uint64, target string, snapshot *restic.Snapshot) (*snapshotLink, error) { return &snapshotLink{root: root, inode: inode, target: target, snapshot: snapshot}, nil } // Readlink func (l *snapshotLink) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) { return l.target, nil } // Attr func (l *snapshotLink) Attr(ctx context.Context, a *fuse.Attr) error { a.Inode = l.inode a.Mode = os.ModeSymlink | 0777 a.Uid = l.root.uid a.Gid = l.root.gid a.Atime = l.snapshot.Time a.Ctime = l.snapshot.Time a.Mtime = l.snapshot.Time a.Nlink = 1 return nil } // Lookup returns a specific entry from the SnapshotsDir. func (d *SnapshotsDir) Lookup(ctx context.Context, name string) (fs.Node, error) { debug.Log("Lookup(%s)", name) sn, ok := d.names[name] if !ok { // could not find entry. Updating repository-state err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update snapshot names updateSnapshotNames(d, d.root.cfg.SnapshotTemplate) sn, ok := d.names[name] if ok { return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn) } if name == "latest" && d.latest != "" { sn, ok := d.names[d.latest] // internal error if !ok { return nil, fuse.ENOENT } return newSnapshotLink(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), d.latest, sn) } return nil, fuse.ENOENT } return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn) } // Lookup returns a specific entry from the SnapshotsIDSDir. func (d *SnapshotsIDSDir) Lookup(ctx context.Context, name string) (fs.Node, error) { debug.Log("Lookup(%s)", name) sn, ok := d.names[name] if !ok { // could not find entry. Updating repository-state err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update snapshot ids updateSnapshotIDSNames(d) sn, ok := d.names[name] if ok { return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn) } return nil, fuse.ENOENT } return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn) } // Lookup returns a specific entry from the HostsDir. func (d *HostsDir) Lookup(ctx context.Context, name string) (fs.Node, error) { debug.Log("Lookup(%s)", name) _, ok := d.hosts[name] if !ok { // could not find entry. Updating repository-state err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update host names updateHostsNames(d) _, ok := d.hosts[name] if ok { return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), "", name), nil } return nil, fuse.ENOENT } return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), "", name), nil } // Lookup returns a specific entry from the TagsDir. func (d *TagsDir) Lookup(ctx context.Context, name string) (fs.Node, error) { debug.Log("Lookup(%s)", name) _, ok := d.tags[name] if !ok { // could not find entry. Updating repository-state err := updateSnapshots(ctx, d.root) if err != nil { return nil, err } // update tag names updateTagNames(d) _, ok := d.tags[name] if ok { return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), name, ""), nil } return nil, fuse.ENOENT } return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), name, ""), nil }