X-Git-Url: https://mattmccutchen.net/utils/utils.git/blobdiff_plain/40ea9b7868f2b7746e7cbabfba6aba982096392a..273c390351c42303171c25215304d1cfd6ca02d4:/ntfsresizecopy diff --git a/ntfsresizecopy b/ntfsresizecopy new file mode 100755 index 0000000..4601695 --- /dev/null +++ b/ntfsresizecopy @@ -0,0 +1,178 @@ +#!/usr/bin/perl +# ntfsresizecopy: Copy an NTFS filesystem from one block device to another, +# resizing it to the size of the destination device in the process. (Uses +# ntfsprogs from http://linux-ntfs.org/doku.php?id=ntfsprogs .) This is +# EXPERIMENTAL; after using this script, you should mount the destination +# read-only and check that everything looks intact. +# +# usage: ntfsresizecopy SRC DEST +# +# An expanding copy is just done with ntfsclone followed by ntfsresize. +# A shrinking copy is done by running ntfsclone and ntfsresize on devices +# specially crafted with the Linux device-mapper (requires dmsetup and losetup); +# you may save time by checking first that the shrinkage is possible with +# `ntfsresize -n -s SIZE SRC'. +# +# The special shrinking technique should be applicable to any filesystem type +# that has an in-place shrinking command that doesn't write outside the new +# size. Just change the calls to ntfsclone and ntfsresize; ntfsclone can be +# replaced by a dd of the beginning of the source for filesystems that don't +# have a sparse clone command. +# +# Version 2008.06.01 +# Maintained at http://mattmccutchen.net/utils/#ntfsresizecopy . +# -- Matt McCutchen + +use strict; +use warnings; +use Fcntl qw(SEEK_SET SEEK_CUR SEEK_END); +use List::Util qw(min); +use filetest 'access'; +$| = 1; + +# These are not currently used but might be useful when modifying this script +# for a filesystem that doesn't have an ntfsclone analogue. +#my $shownProgress = ''; # cursor at its end +#sub showProgress($) { +# my ($newProgress) = @_; +# my $shrink = length($shownProgress) - length($newProgress); +# print("\b" x length($shownProgress), $newProgress, +# $shrink > 0 ? (" " x $shrink, "\b" x $shrink) : ()); +# $shownProgress = $newProgress; +#} +#sub dd(**$) { +# my ($srcfh, $destfh, $len) = @_; +# while ($len > 0) { +# showProgress("$len bytes left"); +# my $chunkLen = min(1048576, $len); +# sysread($srcfh, my $data, $chunkLen) == $chunkLen or die 'read error'; +# syswrite($destfh, $data, $chunkLen) == $chunkLen or die 'write error'; +# $len -= $chunkLen; +# } +# showProgress(''); +#} + +sub deviceSize(*) { + # Determine the size of a device by seeking to its end. The ioctl used + # by `blockdev --getsize64' might be more official, but this one is + # easy and perhaps more portable. + my ($fh) = @_; + my $origPos = sysseek($fh, 0, SEEK_CUR) or die; + my $size = sysseek($fh, 0, SEEK_END) or die; + sysseek($fh, $origPos, SEEK_SET) or die; + return 0 + $size; +} +# Wrappers for dmsetup and losetup +sub dm_create($@) { + my ($name, @table) = @_; + open(my $toDms, '|-', 'dmsetup', 'create', $name) or die; + print $toDms map(join(' ', @{$_}) . "\n", @table); + close($toDms) or die "dmsetup create $name failed"; +} +sub dm_remove($) { + my ($name) = @_; + system('dmsetup', 'remove', $name) and warn "dmsetup remove $name failed"; +} +sub losetup($) { + my ($file) = @_; + open(my $fromLs, '-|', 'losetup', '-fs', $file); + my $dev = <$fromLs>; + close($fromLs) and defined($dev) or die "losetup -fs $file failed"; + chomp($dev); + return $dev; +} +sub losetup_d($) { + my ($dev) = @_; + system('losetup', '-d', $dev) and warn "losetup -d $dev failed"; +} + +scalar(@ARGV) == 2 or die < $dest ($destSize bytes).\n"; + +if ($shrinkBlocks > 0) { + +print "\nSTEP 1: ntfsclone the beginning of the src to the dest.\n"; +# Really, clone the whole src to a magical dest consisting of the real dest +# followed by a zero target to make up the size difference. +# Writes outside the dest's size will be lost to the zero target, but that +# doesn't hurt anything. And under the assumption that the shrinkage is +# possible, ntfsclone copies at most as much data as a simple dd of the +# beginning of the src to the dest would. +my $mdn = "ntfsresizecopy.$$.magicdest"; +my $magicDest = "/dev/mapper/$mdn"; +# If something in the "eval" fails, still clean up as much as possible. +eval { + dm_create($mdn, + [0, $destBlocks, 'linear', $dest, 0], + [$destBlocks, $shrinkBlocks, 'zero']); + system('ntfsclone', '--overwrite', $magicDest, $src) and die 'ntfsclone failed.'; +}; +dm_remove($mdn); +die $@ if $@; + +print "\n", <", undef) or die 'failed to create temporary COW file'; + truncate($cowfh, 4096) or die 'failed to expand temporary COW file'; + $cowdev = losetup("/proc/$$/fd/" . fileno($cowfh)); + dm_create($mdn, + [0, $destBlocks, 'linear', $dest, 0], + [$destBlocks, $shrinkBlocks, 'snapshot', $src, $cowdev, 'N', 1]); + open(my $toNr, '|-', 'ntfsresize', '-s', $destSize, $magicDest) or die 'ntfsresize failed.'; + print $toNr "y\n"; # Confirm automatically because we aren't endangering the src. + close($toNr) or die 'ntfsresize failed.'; +}; +dm_remove($mdn); +losetup_d($cowdev) if defined($cowdev); +die $@ if $@; + +} else { + +print "\nSTEP 1: ntfsclone the src to the dest.\n"; +system('ntfsclone', '--overwrite', $dest, $src) and die 'ntfsclone failed.'; + +print "\n", <