diff --git a/lib/Linux/ExtentsMap.pm b/lib/Linux/ExtentsMap.pm new file mode 100644 index 0000000..8827ec4 --- /dev/null +++ b/lib/Linux/ExtentsMap.pm @@ -0,0 +1,149 @@ +#!/usr/bin/perl +# +# Linux::ExtentsMap - Read and Manipulate List of Extents +# +# Copyright (C) 2014-2019 Axel Burri +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# --------------------------------------------------------------------- +# The official btrbk website is located at: +# https://digint.ch/btrbk/ +# +# Author: +# Axel Burri +# --------------------------------------------------------------------- +# +# Based on work from: +# Graham Cobb: https://github.com/GrahamCobb/extents-lists +# --------------------------------------------------------------------- + +package Linux::ExtentsMap; + +our $blocksize = 4096; # default blocksize + +sub new +{ + my $class = shift; + my $file = shift; + + my $self = { + map => defined($file) ? filefrag_extentmap($file) : [], + }; + bless $self, ref($class) || $class; + return $self->merge; +} + + +# returns extents range (unsorted array of [start,end], inclusive) from FIEMAP ioctl +sub filefrag_extentmap($) +{ + my $file = shift || die; + + # NOTE: this returns exitstatus=0 if file is not found, or no files found + my $ret = `find '$file' -xdev -type f -print0 | xargs -0 -r filefrag -vs`; + return undef unless(defined($ret)); + return undef if($?); + + my @range; + foreach (split(/\n/, $ret)) + { + # get extents start / end + push @range, [ $1, $2 ] if(/^\s*[0-9]+:\s*[0-9]+\.\.\s*[0-9]+:\s*([0-9]+)\.\.\s*([0-9]+):/); + if(/block of ([0-9]+) bytes/) { + die "filefrag reports blocksize=$1 (expected $blocksize) in: $file" if($1 ne $blocksize); + } + } + return \@range; +} + + +sub total_blocks() +{ + my $self = shift; + my $count = 0; + foreach(@{$self->{map}}) { + $count += ($_->[1] - $_->[0] + 1); + } + return $count; +} + + +sub size() +{ + my $self = shift; + my $total_blocks = $self->total_blocks(); + return $total_blocks * $blocksize; +} + + +# merge sorted map +sub merge($) +{ + my $self = shift; + my @merged; + my $start = -1; + my $end = -2; + foreach (sort { $a->[0] <=> $b->[0] } @{$self->{map}}) + { + if($_->[0] <= $end + 1) { + # range overlaps the preceeding one, or is adjacent to it + $end = $_->[1] if($_->[1] > $end); + } + else { + push @merged, [ $start, $end ] if($start >= 0); + $start = $_->[0]; + $end = $_->[1]; + } + } + push @merged, [ $start, $end ] if($start >= 0); + $self->{map} = \@merged; + return $self; +} + + +sub diff($) +{ + my $self = shift; + my $l = $self->{map}; + my $r = (shift)->{map}; + my $i = 0; + my $rn = scalar(@$r); + my @diff; + + foreach(@$l) { + my $l_start = $_->[0]; + my $l_end = $_->[1]; + while(($i < $rn) && ($r->[$i][1] < $l_start)) { # r_end < l_start + # advance r to next overlapping + $i++; + } + while(($i < $rn) && ($r->[$i][0] <= $l_end)) { # r_start <= l_end + # while overlapping, advance l_start + my $r_start = $r->[$i][0]; + my $r_end = $r->[$i][1]; + + push @diff, [ $l_start, $r_start - 1 ] if($l_start < $r_start); + $l_start = $r_end + 1; + last if($l_start > $l_end); + $i++; + } + push @diff, [ $l_start, $l_end ] if($l_start <= $l_end); + } + my $ret = { map => \@diff }; + bless $ret, ref($self); + return $ret; +} + +1;