├── .gitignore ├── MANIFEST ├── Makefile.PL ├── LICENSE ├── README.pod └── stasis /.gitignore: -------------------------------------------------------------------------------- 1 | backup_list 2 | .* 3 | *.old 4 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | LICENSE 2 | Makefile.PL 3 | MANIFEST This list of files 4 | README.pod 5 | stasis 6 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | use 5.008005; 2 | use ExtUtils::MakeMaker; 3 | 4 | WriteMakefile( 5 | NAME => 'stasis', 6 | VERSION_FROM => 'stasis', 7 | ABSTRACT_FROM => 'stasis', 8 | AUTHOR => 'David Farrell', 9 | LICENSE => 'FreeBSD', 10 | EXE_FILES => [qw(stasis)], 11 | PREREQ_PM => { 12 | 'autodie' => 0, 13 | 'Time::Piece' => 0, 14 | 'Time::Seconds' => 0, 15 | 'Getopt::Long' => 0, 16 | 'Pod::Usage' => 0, 17 | }, 18 | (eval { ExtUtils::MakeMaker->VERSION(6.46) } ? (META_MERGE => { 19 | 'meta-spec' => { version => 2 }, 20 | resources => { 21 | repository => { 22 | type => 'git', 23 | url => 'https://github.com/dnmfarrell/Stasis.git', 24 | web => 'https://github.com/dnmfarrell/Stasis', 25 | }, 26 | }}) 27 | : () 28 | ), 29 | ); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is Copyright (c) 2015 by David Farrell. 2 | 3 | This is free software, licensed under: 4 | 5 | The (two-clause) FreeBSD License 6 | 7 | The FreeBSD License 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are 11 | met: 12 | 13 | 1. Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | 2. Redistributions in binary form must reproduce the above copyright 17 | notice, this list of conditions and the following disclaimer in the 18 | documentation and/or other materials provided with the 19 | distribution. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | stasis - an encrypting archive tool using tar, gpg and perl 4 | 5 | =head1 SYNOPSIS 6 | 7 | stasis [options] 8 | 9 | Options: 10 | 11 | --destination -de destination directory to save the encrypted archive to 12 | --days -da only create an archive if one doesn't exist within this many days (optional) 13 | --files -f filepath to a text file of filepaths to backup 14 | --ignore -i filepath to a text file of glob patterns to ignore (optional) 15 | --limit -l limit number of stasis backups to keep in destination directory (optional) 16 | --passphrase passphrase to use 17 | --passfile filepath to a textfile containing the password to use 18 | --referrer -r name of the gpg key to use (instead of a passphrase or passfile) 19 | --temp -t temp directory path, uses /tmp by default 20 | --verbose -v verbose, print progress statements (optional) 21 | --help -h print this documentation (optional) 22 | 23 | =head1 OPTIONS 24 | 25 | =head2 Examples 26 | 27 | Save all the files listed in C (one per line) to Dropbox: 28 | 29 | $ stasis --destination ~/Dropbox --files files_to_backup.txt --passphrase mysecretkey 30 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passphrase mysecretkey 31 | 32 | Use passfile instead of passphrase 33 | 34 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passfile /path/to/passfile 35 | 36 | Use referrer GPG key to encrypt 37 | 38 | $ stasis -de ~/Dropbox -f files_to_backup.txt -r keyname@example.com 39 | 40 | Ignore the files matching patterns in C<.stasisignore> 41 | 42 | $ stasis -de ~/Dropbox -f files_to_backup.txt -r keyname@example.com -i .stasisignore 43 | 44 | Verbose mode 45 | 46 | $ stasis -de ~/Dropbox -f files_to_backup.txt -r mygpgkey@email.com -v 47 | 48 | Only keep the last 4 backups 49 | 50 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passphrase mysecretkey -l 4 51 | 52 | Only make weekly backups 53 | 54 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passphrase mysecretkey --days 7 55 | 56 | =head1 REQUIREMENTS/DEPENDENCIES 57 | 58 | =over 4 59 | 60 | =item * GnuPG 61 | 62 | =item * GNU tar 63 | 64 | =item * Perl 65 | 66 | =back 67 | 68 | C has been tested on Linux with GNU tar v 1.28, GnuPG v1.4.19 and Perl 5.20.2. It should work on many earlier versions too. 69 | 70 | =head1 BUGS/LIMITATIONS 71 | 72 | If C<--files> contains the temp or destination location in it, C will create an infinite loop. 73 | 74 | When a passphrase or file is provided, symmetric GPG encryption is done using AES256 cipher algorithm. At the time of development 75 | this is considered secure, but only as strong as the passphrase used to encrypt the data. 76 | 77 | =head1 INSTALLATION 78 | 79 | $ cpan stasis 80 | 81 | Or 82 | 83 | $ git clone https://github.com/dnmfarrell/Stasis 84 | $ cd Stasis 85 | $ perl Makefile.PL 86 | $ make 87 | $ make install 88 | 89 | =head1 REPOSITORY 90 | 91 | L 92 | 93 | =head1 AUTHOR 94 | 95 | David Farrell E 2015 96 | 97 | =head1 LICENSE 98 | 99 | FreeBSD (2 clause BSD license) 100 | 101 | 102 | =cut 103 | 104 | -------------------------------------------------------------------------------- /stasis: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | package stasis; 3 | package main; 4 | use autodie; 5 | use strict; 6 | use warnings; 7 | use Time::Piece; 8 | use Time::Seconds; 9 | use Getopt::Long 'GetOptions'; 10 | use Pod::Usage 'pod2usage'; 11 | 12 | $SIG{INT} = $SIG{TERM} = sub { exit 1 }; 13 | 14 | our $VERSION = 0.10; 15 | 16 | my $start_time = localtime; 17 | 18 | GetOptions( 19 | 'destination=s' => \ my $destination, 20 | 'files=s' => \ my $file_list, 21 | 'ignore=s' => \ my $ignore, 22 | 'days=i' => \ (my $days = 0), 23 | 'limit=i' => \ my $limit, 24 | 'passphrase=s' => \ my $passphrase, 25 | 'passfile=s' => \ my $passfile, 26 | 'referrer=s' => \ my $referrer, 27 | 'temp=s' => \(my $tmp = '/tmp'), 28 | 'verbose' => \ my $verbose, 29 | 'help|?' => sub { pod2usage(1) }, 30 | ); 31 | 32 | die pod2usage() unless $destination 33 | && $file_list 34 | && ($passphrase || $passfile || $referrer); 35 | 36 | # validate mandatory args 37 | die "--temp $tmp does not exist\n" unless -e $tmp; 38 | die "--destination $destination does not exist\n" unless -e $destination; 39 | die "--files $file_list does not exist\n" unless -e $file_list; 40 | die "--passfile $passfile does not exist\n" if $passfile && ! -e $passfile; 41 | 42 | # check if a backup already exists within x days 43 | unless (need_backup()) 44 | { 45 | log_msg("A backup already exists within $days days"); 46 | exit 0; 47 | } 48 | 49 | my $archive_name = gen_archive_name(); 50 | my $tmp_destination = join('/', $tmp, $archive_name); 51 | my $final_destination = join('/', $destination, $archive_name); 52 | 53 | # read file list 54 | log_msg("\nStasis backup started on $start_time","Reading $file_list"); 55 | open my $files, '<', $file_list or die $!; 56 | my @files; 57 | while (<$files>) 58 | { 59 | chomp; 60 | push @files, $_; 61 | } 62 | log_msg(sprintf "Found %s files/directories to archive", scalar @files); 63 | 64 | # compress 65 | my $rv = compress($tmp_destination, \@files, $ignore); 66 | die "Failed to compress files to $tmp_destination $?\n" unless $rv == 0; 67 | 68 | # encrypt 69 | $rv = encrypt($tmp_destination, $passphrase, $referrer, $passfile, $final_destination); 70 | die "Failed to encrypt $tmp_destination $?\n" unless $rv == 0; 71 | 72 | # clean 73 | clean($destination, $limit); 74 | 75 | my $end_time = localtime; 76 | 77 | log_msg(sprintf "Archiving complete, runtime: %u secs", $end_time - $start_time); 78 | 79 | sub compress 80 | { 81 | my ($destination, $files, $ignore) = @_; 82 | my $args = $verbose ? 'cvzf' : 'czf'; 83 | my $ignore_text = $ignore ? "-X $ignore" : ""; 84 | my $file_list = join(' ', @$files); 85 | 86 | log_msg("Compressing files"); 87 | system("tar $args $destination $ignore_text $file_list"); 88 | } 89 | 90 | sub gen_archive_name 91 | { 92 | my $dt = localtime; 93 | my $name = 'stasis-' . $dt->datetime . '.tar.gz.gpg'; 94 | # replace fat32 illegal characters in timestamp 95 | $name =~ s/:/_/g; 96 | return $name; 97 | } 98 | 99 | sub encrypt 100 | { 101 | my ($source, $passphrase, $referrer, $passfile, $final_destination) = @_; 102 | log_msg("Encrypting archive"); 103 | 104 | if ($passphrase || $passfile) 105 | { 106 | my $pass_arg = $passphrase 107 | ? "--passphrase $passphrase" 108 | : "--passphrase-file $passfile"; 109 | 110 | system("gpg --no-tty --pinentry-mode=loopback -c -o $final_destination --cipher-algo AES256 $pass_arg $source"); 111 | } 112 | else 113 | { 114 | system("gpg --no-tty --pinentry-mode=loopback -e -o $final_destination -R $referrer $source"); 115 | } 116 | } 117 | 118 | sub clean 119 | { 120 | my ($destination, $limit) = @_; 121 | 122 | log_msg("Cleaning files"); 123 | 124 | if (defined $limit) 125 | { 126 | # always keep the latest backup 127 | $limit = 1 if $limit == 0; 128 | 129 | log_verbose("Cleaning backups, limit $limit"); 130 | my @backups = (); 131 | opendir(my $dir, $destination); 132 | 133 | while (readdir $dir) 134 | { 135 | push @backups, "$destination/$_" 136 | if /^stasis-\d\d\d\d-\d\d-\d\dT\d\d_\d\d_\d\d\.tar\.gz\.gpg$/; 137 | } 138 | my @sorted_backups = sort { $b cmp $a } @backups; 139 | my @keep_me = splice(@sorted_backups, 0, $limit); 140 | 141 | # delete remaining backups 142 | log_verbose("Deleting @sorted_backups") if @sorted_backups; 143 | unlink @sorted_backups; 144 | } 145 | } 146 | 147 | sub need_backup 148 | { 149 | my $backup_horizon = $start_time - ONE_DAY * $days; 150 | log_verbose("backup horizon: $backup_horizon"); 151 | 152 | opendir(my $dir, $destination); 153 | while (readdir $dir) 154 | { 155 | next unless /^stasis-.+?\.gpg$/; 156 | my @stat = stat "$destination/$_"; 157 | my $backup_dt = Time::Piece->strptime($stat[9], '%s'); 158 | log_verbose("$destination/$_: $backup_dt"); 159 | 160 | return 0 if $backup_dt > $backup_horizon; 161 | } 162 | return 1; 163 | } 164 | 165 | sub log_msg { print STDERR join("\n", @_, '') } 166 | sub log_verbose { log_msg(@_) if $verbose } 167 | 168 | END { unlink $tmp_destination if $tmp_destination } 169 | 1; 170 | __END__ 171 | =head1 NAME 172 | 173 | stasis - an encrypting archive tool using tar, gpg and perl 174 | 175 | =head1 SYNOPSIS 176 | 177 | stasis [options] 178 | 179 | Options: 180 | 181 | --destination -de destination directory to save the encrypted archive to 182 | --days -da only create an archive if one doesn't exist within this many days (optional) 183 | --files -f filepath to a text file of filepaths to backup 184 | --ignore -i filepath to a text file of glob patterns to ignore (optional) 185 | --limit -l limit number of stasis backups to keep in destination directory (optional) 186 | --passphrase passphrase to use 187 | --passfile filepath to a textfile containing the password to use 188 | --referrer -r name of the gpg key to use (instead of a passphrase or passfile) 189 | --temp -t temp directory path, uses /tmp by default 190 | --verbose -v verbose, print progress statements (optional) 191 | --help -h print this documentation (optional) 192 | 193 | =head1 OPTIONS 194 | 195 | =head2 Examples 196 | 197 | Save all the files listed in C (one per line) to Dropbox: 198 | 199 | $ stasis --destination ~/Dropbox --files files_to_backup.txt --passphrase mysecretkey 200 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passphrase mysecretkey 201 | 202 | Use passfile instead of passphrase 203 | 204 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passfile /path/to/passfile 205 | 206 | Use referrer GPG key to encrypt 207 | 208 | $ stasis -de ~/Dropbox -f files_to_backup.txt -r keyname@example.com 209 | 210 | Ignore the files matching patterns in C<.stasisignore> 211 | 212 | $ stasis -de ~/Dropbox -f files_to_backup.txt -r keyname@example.com -i .stasisignore 213 | 214 | Verbose mode 215 | 216 | $ stasis -de ~/Dropbox -f files_to_backup.txt -r mygpgkey@email.com -v 217 | 218 | Only keep the last 4 backups 219 | 220 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passphrase mysecretkey -l 4 221 | 222 | Only make weekly backups 223 | 224 | $ stasis -de ~/Dropbox -f files_to_backup.txt --passphrase mysecretkey --days 7 225 | 226 | =head1 REQUIREMENTS/DEPENDENCIES 227 | 228 | =over 4 229 | 230 | =item * GnuPG 231 | 232 | =item * GNU tar 233 | 234 | =item * Perl 235 | 236 | =back 237 | 238 | C has been tested on Linux with GNU tar v 1.28, GnuPG v1.4.19 and Perl 5.20.2. It should work on many earlier versions too. 239 | 240 | =head1 BUGS/LIMITATIONS 241 | 242 | If C<--files> contains the temp or destination location in it, C will create an infinite loop. 243 | 244 | When a passphrase or file is provided, symmetric GPG encryption is done using AES256 cipher algorithm. At the time of development 245 | this is considered secure, but only as strong as the passphrase used to encrypt the data. 246 | 247 | =head1 INSTALLATION 248 | 249 | $ cpan stasis 250 | 251 | Or 252 | 253 | $ git clone https://github.com/dnmfarrell/Stasis 254 | $ cd Stasis 255 | $ perl Makefile.PL 256 | $ make 257 | $ make install 258 | 259 | =head1 REPOSITORY 260 | 261 | L 262 | 263 | =head1 AUTHOR 264 | 265 | David Farrell E 2015 266 | 267 | =head1 LICENSE 268 | 269 | FreeBSD (2 clause BSD license) 270 | 271 | =cut 272 | --------------------------------------------------------------------------------