Skip to content

Commit

Permalink
macos: mount snapshots - UNTESTED!
Browse files Browse the repository at this point in the history
  - when the flag '-mount-snapshots' was given
  - and the snapshot is not mounted, mount it

this does not work under linux and freebsd:

    $ zfs mount pool/ds@snap
    cannot open 'pool/ds@snap': snapshot delimiter '@' is not expected here

the used command is somethink like: `zfs mount pool/ds@snap`
see: https://openzfsonosx.org/wiki/FAQ#Q.29_How_can_I_access_the_.zfs_snapshot_directories.3F
  • Loading branch information
j-keck committed Jan 27, 2020
1 parent 1f4c557 commit 176eebe
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 34 deletions.
2 changes: 1 addition & 1 deletion cmd/zfs-snap-diff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func parseFlags() (CliConfig, config.Config) {
// zfs
zfsCfg := &zsdCfg.ZFS
flag.BoolVar(&zfsCfg.UseSudo, "use-sudo", zfsCfg.UseSudo, "use sudo when executing 'zfs' commands")
flag.BoolVar(&zfsCfg.MountSnapshot, "mount-snapshot", zfsCfg.MountSnapshot,
flag.BoolVar(&zfsCfg.MountSnapshots, "mount-snapshots", zfsCfg.MountSnapshots,
"mount snapshot (only necessary if it's not mounted by zfs automatically")

flag.Parse()
Expand Down
10 changes: 5 additions & 5 deletions pkg/config/zfs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import (
)

type ZFSConfig struct {
UseSudo bool
MountSnapshot bool
UseSudo bool
MountSnapshots bool
}

func NewDefaultZFSConfig() ZFSConfig {
mountSnapshot := false
mountSnapshots := false
if runtime.GOOS == "darwin" {
mountSnapshot = true
mountSnapshots = true
}

return ZFSConfig{
UseSudo: false,
MountSnapshot: mountSnapshot,
MountSnapshots: mountSnapshots,
}
}
56 changes: 41 additions & 15 deletions pkg/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (
var log = plog.GlobalLogger()

type Scanner struct {
dateRange DateRange
compareMethod string
dataset zfs.Dataset
dateRange DateRange
compareMethod string
dataset zfs.Dataset
zfs zfs.ZFS
}

type ScanResult struct {
Expand All @@ -32,8 +33,8 @@ type FileVersion struct {
Snapshot zfs.Snapshot `json:"snapshot"`
}

func NewScanner(dateRange DateRange, compareMethod string, dataset zfs.Dataset) Scanner {
return Scanner{dateRange, compareMethod, dataset}
func NewScanner(dateRange DateRange, compareMethod string, dataset zfs.Dataset, zfs zfs.ZFS) Scanner {
return Scanner{dateRange, compareMethod, dataset, zfs}
}

func (self *Scanner) FindFileVersions(pathActualVersion string) (ScanResult, error) {
Expand All @@ -50,6 +51,9 @@ func (self *Scanner) FindFileVersions(pathActualVersion string) (ScanResult, err
var cmp Comparator
snapsSkipped := 0
for idx, snap := range snaps {

// search is data-range based - check if the actual checked snapshot
// was created in the given range
if self.dateRange.IsBefore(snap.Created) {
snapsSkipped = snapsSkipped + 1
log.Tracef("skip snapshot - snapshot is younger (%s) than the time-range: %s",
Expand All @@ -58,8 +62,31 @@ func (self *Scanner) FindFileVersions(pathActualVersion string) (ScanResult, err
continue
}

if self.dateRange.IsAfter(snap.Created) {
log.Debugf("abort search - snapshot is older (%s) than the time-range %s",
snap.Created, self.dateRange.String())
break
}

// mount the snapshot if necessary
if self.zfs.MountSnapshots() {
isMounted, err := snap.IsMounted()
if err != nil {
log.Errorf("unable to check if snapshot: %s is mounted - %v", snap.Name, err)
}

if ! isMounted {
if err := self.zfs.MountSnapshot(snap); err != nil {
log.Errorf("unable to mount snapshot: %s - %v", snap.Name, err)

// skip this snapshot
continue
}
}
}

// initialize the file-content comparator
if cmp == nil {
// init comparator

var pathInitVersion string
if p, ok := self.findLastPathInSnap(pathActualVersion, idx-1, snaps); ok {
Expand All @@ -79,27 +106,26 @@ func (self *Scanner) FindFileVersions(pathActualVersion string) (ScanResult, err
}
}

if self.dateRange.IsAfter(snap.Created) {
log.Debugf("abort search - snapshot is older (%s) than the time-range %s",
snap.Created, self.dateRange.String())
break
}

sr.SnapsScanned = sr.SnapsScanned + 1
sr.LastScannedSnapshot = snap

// get the file-handle to the backup version in the snapshot
fh, err := fs.NewFileHandle(self.pathInSnapshot(pathActualVersion, snap))
if err != nil {
// not every snapshot has a version of the file - ignore errors
// not every snapshot MUST have a version of the file.
// maybe the file was deleted and restored - so ignore the error
sr.SnapsFileMissing = sr.SnapsFileMissing + 1
continue
}

// compare the file content
log.Tracef("check if file was changed under path: %s", fh.Path)
if cmp.HasChanged(fh) {
log.Debugf("file was changed in snapshot: %s", fh.Path)
sr.FileVersions = append(sr.FileVersions, FileVersion{fh, snap})
}

// update stats
sr.SnapsScanned = sr.SnapsScanned + 1
sr.LastScannedSnapshot = snap
}

sr.ScanDuration = time.Now().Sub(startTs)
Expand Down
2 changes: 1 addition & 1 deletion pkg/webapp/webapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (self *WebApp) findFileVersionsHndl(w http.ResponseWriter, r *http.Request)
}

// scan for other file versions
sc := scanner.NewScanner(payload.DateRange, payload.CompareMethod, ds)
sc := scanner.NewScanner(payload.DateRange, payload.CompareMethod, ds, self.zfs)
versions, err := sc.FindFileVersions(payload.Path)
if err != nil {
log.Errorf("File versions search failed - %v", err)
Expand Down
13 changes: 7 additions & 6 deletions pkg/zfs/dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,24 @@ func (self *Dataset) ScanSnapshots() (Snapshots, error) {

snapshots := Snapshots{}
for _, line := range strings.Split(stdout, "\n") {
if snapName, creation, ok := parse(line); ok {
if fullName, creation, ok := parse(line); ok {
// remove dataset name from snapshot
fields := strings.Split(snapName, "@")
snapName := fields[len(fields)-1]
fields := strings.Split(fullName, "@")
name := fields[len(fields)-1]

// create the dir-handle per hand.
// this prevents a unnecessary 'os.Stat' call for each snapshot
// (which takes some time with thousands snapshots on spinning disks)
dir := fs.DirHandle{fs.FSHandle{
Name: snapName,
Path: self.MountPoint.Path + "/.zfs/snapshot/" + snapName,
Name: name,
Path: self.MountPoint.Path + "/.zfs/snapshot/" + name,
Kind: fs.DIR,
Size: 0,
ModTime: creation,
}}

// append new snap to snapshots
snapshots = append(snapshots, Snapshot{snapName, creation, dir})
snapshots = append(snapshots, Snapshot{name, fullName, creation, dir})
}

}
Expand Down
31 changes: 28 additions & 3 deletions pkg/zfs/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,39 @@ package zfs
import (
"github.com/j-keck/zfs-snap-diff/pkg/fs"
"time"
"os"
)

// Snapshot - zfs snapshot
type Snapshot struct {
Name string `json:"name"`
Created time.Time `json:"created"`
Dir fs.DirHandle `json:"dir"`
Name string `json:"name"`
FullName string `json:"fullName"`
Created time.Time `json:"created"`
Dir fs.DirHandle `json:"dir"`
}


// Check if the snaphot is mounted
func (s *Snapshot) IsMounted() (bool, error) {
log.Tracef("check if snapshot: %s is mounted", s.Name)

// to check if the snapshot is mounted, list
// the directory content. use 'File.Readdirnames'
// for the directrory listing, because it has less overhead

fh, err := os.Open(s.Dir.Path)
if err != nil {
return false, err
}

names, _ := fh.Readdirnames(10)
isMounted := len(names) > 0
log.Tracef("snapshot: %s is mounted = %v", s.Name, isMounted)
return isMounted, nil
}



// Snapshots represents snapshots from a zfs dataset
type Snapshots []Snapshot

Expand All @@ -34,3 +58,4 @@ func (s *Snapshots) Filter(f func(Snapshot) bool) Snapshots {
}
return newS
}

15 changes: 15 additions & 0 deletions pkg/zfs/zfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var log = plog.GlobalLogger()
type ZFS struct {
datasets Datasets
cmd ZFSCmd
cfg config.Config
}

// NewZFS returns a handler for a zfs filesystem
Expand All @@ -29,6 +30,7 @@ func NewZFS(name string, cfg config.Config) (ZFS, error) {
return self, err
}
self.datasets = datasets
self.cfg = cfg
return self, nil
}

Expand Down Expand Up @@ -142,6 +144,19 @@ func (self *ZFS) scanDatasets(name string) (Datasets, error) {
return datasets, nil
}


func (self *ZFS) MountSnapshots() bool {
return self.cfg.ZFS.MountSnapshots
}

func (self *ZFS) MountSnapshot(snap Snapshot) error {
log.Debugf("mount snapshot: %s", snap.Name)
stdout, stderr, err := self.cmd.Exec("mount", snap.FullName)
log.Tracef("mount snapshot stdout: %s", stdout)
log.Tracef("mount snapshot stderr: %s", stderr)
return err
}

type SortByPathDesc Datasets

func (s SortByPathDesc) Len() int {
Expand Down
7 changes: 4 additions & 3 deletions webapp/src/ZSD/Model/Snapshot.purs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import ZSD.Model.DateTime (DateTime)
import ZSD.Model.FSEntry (FSEntry)

type Snapshot =
{ name :: String
, created :: DateTime
, dir :: FSEntry
{ name :: String
, fullName :: String
, created :: DateTime
, dir :: FSEntry
}


Expand Down

0 comments on commit 176eebe

Please sign in to comment.