diff --git a/ChangeLog b/ChangeLog
index 67dc38d..0770bae 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,8 @@
btrbk-current
+ * MIGRATION
+ - If you are using raw targets, make sure to run the
+ "raw_suffix2sidecar" utility in each target directory.
* Add "resume" command, replacement for "-r, --resume-only" command
line option (which is now deprecated).
* Add "snapshot" command (close #150).
diff --git a/Makefile b/Makefile
index 8ed0db9..6f0ba17 100644
--- a/Makefile
+++ b/Makefile
@@ -42,6 +42,7 @@ install-share:
@echo 'installing auxiliary scripts...'
install -pDm755 ssh_filter_btrbk.sh "$(DESTDIR)$(SCRIPTDIR)/ssh_filter_btrbk.sh"
install -pDm755 contrib/cron/btrbk-mail "$(DESTDIR)$(SCRIPTDIR)/btrbk-mail"
+ install -pDm755 contrib/migration/raw_suffix2sidecar "$(DESTDIR)$(SCRIPTDIR)/raw_suffix2sidecar"
install-man:
@echo 'installing manpages...'
diff --git a/contrib/migration/raw_suffix2sidecar b/contrib/migration/raw_suffix2sidecar
new file mode 100755
index 0000000..ae15362
--- /dev/null
+++ b/contrib/migration/raw_suffix2sidecar
@@ -0,0 +1,181 @@
+#!/usr/bin/perl
+#
+# raw_suffix2sidecar - migrate to btrbk raw target sidecar files
+#
+# Copyright (C) 2017 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:
+# http://digint.ch/btrbk/
+#
+# Author:
+# Axel Burri
+# ---------------------------------------------------------------------
+
+# Create raw sidecar ".info" files from uuid-suffixed raw backup files
+# generated by btrbk < v0.26.0.
+
+use strict;
+use warnings FATAL => qw( all );
+use Getopt::Long qw(GetOptions);
+
+our $VERSION = '0.26.0-dev'; # match btrbk version
+our $AUTHOR = 'Axel Burri ';
+our $PROJECT_HOME = '';
+
+my $VERSION_INFO = "raw_suffix2sidecar (btrbk migration script), version $VERSION";
+
+my $compress_format_alt = 'gz|bz2|xz|lzo|lz4';
+my $file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout:
+my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
+my $timestamp_postfix_match = qr/\.(?[0-9]{4})(?[0-9]{2})(?[0-9]{2})(T(?[0-9]{2})(?[0-9]{2})((?[0-9]{2})(?(Z|[+-][0-9]{4})))?)?(_(?[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]"
+my $raw_postfix_match = qr/--(?$uuid_match)(\@(?$uuid_match))?\.btrfs?(\.(?($compress_format_alt)))?(\.(?gpg))?(\.(?split_aa))?(\.(?part))?/; # matches ".btrfs_[@][.gz|bz2|xz][.gpg][.split_aa][.part]"
+
+my $dryrun;
+
+my %raw_info_sort = (
+ TYPE => 1,
+ FILE => 2,
+ RECEIVED_UUID => 3,
+ RECEIVED_PARENT_UUID => 4,
+ INCOMPLETE => 5,
+ compress => 9,
+ split => 10,
+ encrypt => 11,
+ );
+
+sub VERSION_MESSAGE
+{
+ print STDERR $VERSION_INFO . "\n\n";
+}
+
+sub HELP_MESSAGE
+{
+ print STDERR "usage: raw_suffix2sidecar ...\n";
+ print STDERR "\n";
+ print STDERR "options:\n";
+ # "--------------------------------------------------------------------------------"; # 80
+ print STDERR " -h, --help display this help message\n";
+ print STDERR " --version display version information\n";
+ print STDERR " -n, --dry-run perform a trial run with no changes made\n";
+ print STDERR "\n";
+ print STDERR "For additional information, see $PROJECT_HOME\n";
+}
+
+sub write_raw_info($$)
+{
+ my $file = shift // die;
+ my $raw_info = shift // die;
+
+ my $info_file = $file . '.info';
+ my @line;
+ push @line, "#raw_suffix2sidecar-v$VERSION";
+ push @line, "# Do not edit this file";
+
+ # sort by %raw_info_sort, then by key
+ foreach(sort { (($raw_info_sort{$a} || 99) <=> ($raw_info_sort{$b} || 99)) || ($a cmp $b) } keys %$raw_info) {
+ push @line, ($_ . '=' . $raw_info->{$_}) if($raw_info->{$_});
+ }
+
+ print "Creating info file: $info_file\n";
+
+ unless($dryrun) {
+ open (INFOFILE, ">> $info_file") || die "Failed to open $info_file";
+ print INFOFILE join("\n", @line) . "\n";
+ close(INFOFILE);
+ }
+
+ return $info_file;
+}
+
+
+MAIN:
+{
+ Getopt::Long::Configure qw(gnu_getopt);
+ unless(GetOptions(
+ 'help|h' => sub { VERSION_MESSAGE(); HELP_MESSAGE(0); exit 0; },
+ 'version' => sub { VERSION_MESSAGE(); exit 0; },
+ 'dry-run|n' => \$dryrun,
+ ))
+ {
+ VERSION_MESSAGE();
+ HELP_MESSAGE(0);
+ exit 2;
+ }
+ unless(@ARGV) {
+ VERSION_MESSAGE();
+ HELP_MESSAGE();
+ exit 1;
+ }
+
+ foreach my $target_dir (@ARGV) {
+ $target_dir =~ s/\/+$//;
+ print "Processing directory: $target_dir/\n";
+ opendir(my($dh), $target_dir) || die "Failed to open directory '$target_dir': $!";
+ my @files = readdir($dh);
+ closedir $dh;
+
+ my @splitfiles = @files;
+ foreach my $file (@files) {
+ if($file =~ /^(?$file_match$timestamp_postfix_match)$raw_postfix_match$/) {
+ print "\nProcessing raw backup: $file\n";
+
+ my $newname = $+{basename} || die;
+ my %raw_info = (
+ TYPE => 'raw',
+ RECEIVED_UUID => $+{received_uuid},
+ RECEIVED_PARENT_UUID => $+{parent_uuid},
+ INCOMPLETE => $+{incomplete} ? 1 : 0,
+ compress => $+{compress},
+ split => ($+{split} ? (-s $file) : undef), # file size
+ encrypt => $+{encrypt},
+ );
+ die "Missing received uuid in file: $file" unless $raw_info{RECEIVED_UUID};
+ $newname .= '.btrfs';
+ $newname .= '.' . $raw_info{compress} if($raw_info{compress});
+ $newname .= '.' . $raw_info{encrypt} if($raw_info{encrypt});
+ $raw_info{FILE} = $newname;
+ write_raw_info("$target_dir/$newname", \%raw_info);
+
+ if($raw_info{split}) {
+ my $sfile = $file;
+ $sfile =~ s/_aa$//; # we match on ".split_aa" above
+ foreach my $splitfile (@splitfiles) {
+ if($splitfile =~ /^${sfile}(_[a-z]+)$/) {
+ my $suffix = $1 // die;
+ print "Renaming file: $target_dir/$splitfile -> $target_dir/$newname.split$suffix\n";
+ unless($dryrun) {
+ rename("$target_dir/$splitfile", "$target_dir/$newname.split$suffix") || die "Failed to rename file: $target_dir/$splitfile -> $target_dir/${newname}.split$suffix: $!";
+ }
+ }
+ }
+ }
+ else {
+ print "Renaming file: $target_dir/$file -> $target_dir/$newname\n";
+ unless($dryrun) {
+ rename("$target_dir/$file", "$target_dir/$newname") || die "Failed to rename file: $target_dir/$file -> $target_dir/$newname";
+ }
+ }
+ }
+ }
+ }
+
+ if($dryrun) {
+ print "\nNOTE: Dryrun was active, none of the operations above were actually executed!\n";
+ }
+}
+
+1;