#!/usr/bin/perl # this software is licensed for use under the Free Software Foundation's GPL v3.0 license, as retrieved # from http://www.gnu.org/licenses/gpl-3.0.html on 2014-11-17. A copy should also be available in this # project's Git repository at https://github.com/jimsalterjrs/sanoid/blob/master/LICENSE. use strict; use warnings; my $zfs = '/sbin/zfs'; my %args = getargs(@ARGV); my $progversion = '1.4.7'; if ($args{'version'}) { print "$progversion\n"; exit 0; } my $dataset = getdataset($args{'path'}); my %versions = getversions($args{'path'}, $dataset); foreach my $version (sort { $versions{$a}{'mtime'} <=> $versions{$b}{'mtime'} } keys %versions) { my $disptime = localtime($versions{$version}{'mtime'}); my $dispsize = humansize($versions{$version}{'size'}); print "$disptime\t$dispsize\t$version\n"; } exit 0; ################################################################### ################################################################### ################################################################### sub humansize { my ($rawsize) = @_; my $humansize; if ($rawsize > 1024*1024*1024) { $humansize = sprintf("%.1f",$rawsize/1024/1024/1024) . ' GB'; } elsif ($rawsize > 1024*1024) { $humansize = sprintf("%.1f",$rawsize/1024/1024) . ' MB'; } elsif ($rawsize > 255) { $humansize = sprintf("%.1f",$rawsize/1024) . ' KB'; } else { $humansize = $rawsize . ' Bytes'; } return $humansize; } sub getversions { my ($path, $dataset) = @_; my @snaps = findsnaps($dataset, $args{'path'}); my $snappath = '.zfs/snapshot'; my $relpath = $path; $relpath =~ s/^$dataset\///; my %versions; foreach my $snap (@snaps) { my $filename = "$dataset/$snappath/$snap/$relpath"; my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($filename); # only push to the $versions hash if this size and mtime aren't already present (simple dedupe) my $duplicate = 0; foreach my $version (keys %versions) { if ($versions{$version}{'size'} eq $size && $versions{$version}{'mtime'} eq $mtime) { $duplicate = 1; } } if (! $duplicate) { $versions{$filename}{'size'} = $size; $versions{$filename}{'mtime'} = $mtime; } } return %versions; } sub findsnaps { my ($dataset, $path) = @_; my $snappath = '.zfs/snapshot'; my $relpath = $path; $relpath =~ s/^$dataset//; my @snaps; opendir (my $dh, "$dataset/$snappath"); while (my $dir=(readdir $dh)) { if ($dir ne '.' && $dir ne '..') { push @snaps, $dir; } } closedir $dh; return @snaps; } sub getdataset { my ($path) = @_; open FH, "$zfs list -Ho mountpoint |"; my @datasets = ; close FH; my @matchingdatasets; foreach my $dataset (@datasets) { chomp $dataset; if ( $path =~ /^$dataset/ ) { push @matchingdatasets, $dataset; } } my $bestmatch = ''; foreach my $dataset (@matchingdatasets) { if ( length $dataset > length $bestmatch ) { $bestmatch = $dataset; } } return $bestmatch; } sub getargs { my @args = @_; my %args; my %novaluearg; my %validarg; push my @validargs, ('debug','version'); foreach my $item (@validargs) { $validarg{$item} = 1; } push my @novalueargs, ('debug','version'); foreach my $item (@novalueargs) { $novaluearg{$item} = 1; } while (my $rawarg = shift(@args)) { my $arg = $rawarg; my $argvalue; if ($rawarg =~ /=/) { # user specified the value for a CLI argument with = # instead of with blank space. separate appropriately. $argvalue = $arg; $arg =~ s/=.*$//; $argvalue =~ s/^.*=//; } if ($rawarg =~ /^--/) { # doubledash arg $arg =~ s/^--//; if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; } if ($novaluearg{$arg}) { $args{$arg} = 1; } else { # if this CLI arg takes a user-specified value and # we don't already have it, then the user must have # specified with a space, so pull in the next value # from the array as this value rather than as the # next argument. if ($argvalue eq '') { $argvalue = shift(@args); } $args{$arg} = $argvalue; } } elsif ($arg =~ /^-/) { # singledash arg $arg =~ s/^-//; if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; } if ($novaluearg{$arg}) { $args{$arg} = 1; } else { # if this CLI arg takes a user-specified value and # we don't already have it, then the user must have # specified with a space, so pull in the next value # from the array as this value rather than as the # next argument. if ($argvalue eq '') { $argvalue = shift(@args); } $args{$arg} = $argvalue; } } else { # bare arg $args{'path'} = $arg; } } return %args; }