webtranslateit-safe 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +3 -0
- data/.document +5 -0
- data/.github/dependabot.yml +26 -0
- data/.github/release-drafter.yml +36 -0
- data/.github/workflows/ci.yml +51 -0
- data/.github/workflows/release-drafter.yml +29 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.rubocop_todo.yml +552 -0
- data/CHANGELOG +42 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +89 -0
- data/LICENSE.txt +22 -0
- data/README.markdown +237 -0
- data/Rakefile +8 -0
- data/TODO +31 -0
- data/bin/webtranslateit-safe +64 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/lib/webtranslateit/safe/archive.rb +29 -0
- data/lib/webtranslateit/safe/backup.rb +27 -0
- data/lib/webtranslateit/safe/cloudfiles.rb +77 -0
- data/lib/webtranslateit/safe/config/builder.rb +100 -0
- data/lib/webtranslateit/safe/config/node.rb +79 -0
- data/lib/webtranslateit/safe/ftp.rb +85 -0
- data/lib/webtranslateit/safe/gpg.rb +52 -0
- data/lib/webtranslateit/safe/gzip.rb +29 -0
- data/lib/webtranslateit/safe/local.rb +55 -0
- data/lib/webtranslateit/safe/mongodump.rb +30 -0
- data/lib/webtranslateit/safe/mysqldump.rb +36 -0
- data/lib/webtranslateit/safe/pgdump.rb +36 -0
- data/lib/webtranslateit/safe/pipe.rb +23 -0
- data/lib/webtranslateit/safe/s3.rb +80 -0
- data/lib/webtranslateit/safe/sftp.rb +96 -0
- data/lib/webtranslateit/safe/sink.rb +40 -0
- data/lib/webtranslateit/safe/source.rb +51 -0
- data/lib/webtranslateit/safe/stream.rb +40 -0
- data/lib/webtranslateit/safe/svndump.rb +17 -0
- data/lib/webtranslateit/safe/tmp_file.rb +53 -0
- data/lib/webtranslateit/safe/version.rb +9 -0
- data/lib/webtranslateit/safe.rb +70 -0
- data/spec/integration/archive_integration_spec.rb +89 -0
- data/spec/integration/cleanup_spec.rb +62 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/webtranslateit/safe/archive_spec.rb +67 -0
- data/spec/webtranslateit/safe/cloudfiles_spec.rb +175 -0
- data/spec/webtranslateit/safe/config_spec.rb +307 -0
- data/spec/webtranslateit/safe/gpg_spec.rb +148 -0
- data/spec/webtranslateit/safe/gzip_spec.rb +64 -0
- data/spec/webtranslateit/safe/local_spec.rb +109 -0
- data/spec/webtranslateit/safe/mongodump_spec.rb +54 -0
- data/spec/webtranslateit/safe/mysqldump_spec.rb +83 -0
- data/spec/webtranslateit/safe/pgdump_spec.rb +45 -0
- data/spec/webtranslateit/safe/s3_spec.rb +168 -0
- data/spec/webtranslateit/safe/svndump_spec.rb +39 -0
- data/templates/script.rb +183 -0
- data/webtranslateit-safe.gemspec +32 -0
- metadata +149 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'webtranslateit/safe/config/builder'
|
2
|
+
module WebTranslateIt
|
3
|
+
|
4
|
+
module Safe
|
5
|
+
|
6
|
+
module Config
|
7
|
+
|
8
|
+
class Node
|
9
|
+
|
10
|
+
attr_reader :parent, :data
|
11
|
+
|
12
|
+
def initialize(parent = nil, data = {}, &)
|
13
|
+
@parent = parent
|
14
|
+
@data = {}
|
15
|
+
merge(data, &)
|
16
|
+
end
|
17
|
+
|
18
|
+
def merge(data = {}, &block)
|
19
|
+
builder = Builder.new(self, data)
|
20
|
+
builder.instance_eval(&block) if block
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# looks for the path from this node DOWN. will not delegate to parent
|
25
|
+
def get(*path)
|
26
|
+
key = path.shift
|
27
|
+
value = @data[key.to_s]
|
28
|
+
return value if !value.nil? && path.empty?
|
29
|
+
|
30
|
+
value&.get(*path)
|
31
|
+
end
|
32
|
+
|
33
|
+
# recursive find
|
34
|
+
# starts at the node and continues to the parent
|
35
|
+
def find(*path)
|
36
|
+
get(*path) || @parent&.find(*path)
|
37
|
+
end
|
38
|
+
alias [] find
|
39
|
+
|
40
|
+
def set_multi(key, value)
|
41
|
+
@data[key.to_s] ||= []
|
42
|
+
@data[key.to_s].push(*value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def set(key, value)
|
46
|
+
@data[key.to_s] = value
|
47
|
+
end
|
48
|
+
alias []= set
|
49
|
+
|
50
|
+
def each(&)
|
51
|
+
@data.each(&)
|
52
|
+
end
|
53
|
+
include Enumerable
|
54
|
+
|
55
|
+
def to_hash
|
56
|
+
@data.keys.each_with_object({}) do |key, res|
|
57
|
+
value = @data[key]
|
58
|
+
res[key] = value.is_a?(Node) ? value.to_hash : value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def dump(indent = '')
|
63
|
+
@data.each do |key, value|
|
64
|
+
if value.is_a?(Node)
|
65
|
+
puts "#{indent}#{key}:"
|
66
|
+
value.dump("#{indent} ")
|
67
|
+
else
|
68
|
+
puts "#{indent}#{key}: #{value.inspect}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Ftp < Sink
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
def active?
|
10
|
+
host && user
|
11
|
+
end
|
12
|
+
|
13
|
+
def path
|
14
|
+
@path ||= expand(config[:ftp, :path] || config[:local, :path] || ':kind/:id')
|
15
|
+
end
|
16
|
+
|
17
|
+
def save
|
18
|
+
raise 'pipe-streaming not supported for FTP.' unless @backup.path
|
19
|
+
|
20
|
+
puts "Uploading #{host}:#{full_path} via FTP" if verbose? || dry_run?
|
21
|
+
|
22
|
+
return if dry_run? || local_only?
|
23
|
+
|
24
|
+
port ||= 21
|
25
|
+
Net::FTP.open(host) do |ftp|
|
26
|
+
ftp.connect(host, port)
|
27
|
+
ftp.login(user, password)
|
28
|
+
puts "Sending #{@backup.path} to #{full_path}" if verbose?
|
29
|
+
begin
|
30
|
+
ftp.put(@backup.path, full_path)
|
31
|
+
rescue Net::FTPPermError
|
32
|
+
puts "Ensuring remote path (#{path}) exists" if verbose?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
puts '...done' if verbose?
|
36
|
+
end
|
37
|
+
|
38
|
+
def cleanup
|
39
|
+
return if local_only? || dry_run?
|
40
|
+
|
41
|
+
return unless (keep = config[:keep, :ftp])
|
42
|
+
|
43
|
+
puts "listing files: #{host}:#{base}*" if verbose?
|
44
|
+
port ||= 21
|
45
|
+
Net::FTP.open(host) do |ftp|
|
46
|
+
ftp.connect(host, port)
|
47
|
+
ftp.login(user, password)
|
48
|
+
files = ftp.nlst(path)
|
49
|
+
pattern = File.basename(base.to_s)
|
50
|
+
files = files.select { |x| x.start_with?(pattern) }
|
51
|
+
puts(files.collect { |x| x }) if verbose?
|
52
|
+
|
53
|
+
files = files
|
54
|
+
.collect { |x| x }
|
55
|
+
.sort
|
56
|
+
|
57
|
+
cleanup_with_limit(files, keep) do |f|
|
58
|
+
file = File.join(path, f)
|
59
|
+
puts "removing ftp file #{host}:#{file}" if dry_run? || verbose?
|
60
|
+
ftp.delete(file) unless dry_run? || local_only?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def host
|
66
|
+
config[:ftp, :host]
|
67
|
+
end
|
68
|
+
|
69
|
+
def user
|
70
|
+
config[:ftp, :user]
|
71
|
+
end
|
72
|
+
|
73
|
+
def password
|
74
|
+
config[:ftp, :password]
|
75
|
+
end
|
76
|
+
|
77
|
+
def port
|
78
|
+
config[:ftp, :port]
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Gpg < Pipe
|
6
|
+
|
7
|
+
def active?
|
8
|
+
raise "can't use both gpg password and pubkey" if key && password
|
9
|
+
|
10
|
+
!!(password || key)
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def post_process
|
16
|
+
@backup.compressed = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def pipe
|
20
|
+
command = config[:gpg, :command] || 'gpg'
|
21
|
+
if key
|
22
|
+
"|#{command} #{config[:gpg, :options]} -e -r #{key}"
|
23
|
+
elsif password
|
24
|
+
"|#{command} #{config[:gpg, :options]} -c --passphrase-file #{gpg_password_file(password)}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def extension
|
29
|
+
'.gpg'
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def password
|
35
|
+
@password ||= config[:gpg, :password]
|
36
|
+
end
|
37
|
+
|
38
|
+
def key
|
39
|
+
@key ||= config[:gpg, :key]
|
40
|
+
end
|
41
|
+
|
42
|
+
def gpg_password_file(pass)
|
43
|
+
return 'TEMP_GENERATED_FILENAME' if dry_run?
|
44
|
+
|
45
|
+
WebTranslateIt::Safe::TmpFile.create('gpg-pass') { |file| file.write(pass) }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Gzip < Pipe
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
def post_process
|
10
|
+
@backup.compressed = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def pipe
|
14
|
+
'|gzip'
|
15
|
+
end
|
16
|
+
|
17
|
+
def extension
|
18
|
+
'.gz'
|
19
|
+
end
|
20
|
+
|
21
|
+
def active?
|
22
|
+
!@backup.compressed
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Local < Sink
|
6
|
+
|
7
|
+
def active?
|
8
|
+
# S3 can't upload from pipe. it needs to know file size, so we must pass through :local
|
9
|
+
# will change once we add SSH/FTP sink
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def path
|
16
|
+
@path ||= File.expand_path(expand(config[:local, :path] || raise('missing :local/:path')))
|
17
|
+
end
|
18
|
+
|
19
|
+
def save
|
20
|
+
puts "command: #{@backup.command}" if verbose?
|
21
|
+
|
22
|
+
# FIXME: probably need to change this to smth like @backup.finalize!
|
23
|
+
@backup.path = full_path # need to do it outside DRY_RUN so that it will be avialable for S3 DRY_RUN
|
24
|
+
|
25
|
+
return if dry_run?
|
26
|
+
|
27
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
28
|
+
benchmark = Benchmark.realtime do
|
29
|
+
system "#{@backup.command}>#{@backup.path}"
|
30
|
+
end
|
31
|
+
puts("command took #{format('%.2f', benchmark)} second(s).") if verbose?
|
32
|
+
end
|
33
|
+
|
34
|
+
def cleanup
|
35
|
+
return unless (keep = config[:keep, :local])
|
36
|
+
|
37
|
+
puts "listing files #{base}" if verbose?
|
38
|
+
|
39
|
+
# TODO: cleanup ALL zero-length files
|
40
|
+
|
41
|
+
files = Dir["#{base}*"]
|
42
|
+
.select { |f| File.file?(f) && File.size(f).positive? }
|
43
|
+
.sort
|
44
|
+
|
45
|
+
cleanup_with_limit(files, keep) do |f|
|
46
|
+
puts "removing local file #{f}" if dry_run? || verbose?
|
47
|
+
File.unlink(f) unless dry_run?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Mongodump < Source
|
6
|
+
|
7
|
+
|
8
|
+
def command
|
9
|
+
opts = []
|
10
|
+
opts << "--host #{config[:host]}" if config[:host]
|
11
|
+
opts << "-u #{config[:user]}" if config[:user]
|
12
|
+
opts << "-p #{config[:password]}" if config[:password]
|
13
|
+
opts << "--out #{output_directory}"
|
14
|
+
|
15
|
+
"mongodump -q \"{xxxx : { \\$ne : 0 } }\" --db #{@id} #{opts.join(' ')} && cd #{output_directory} && tar cf - ."
|
16
|
+
end
|
17
|
+
|
18
|
+
def extension = '.tar'
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def output_directory
|
23
|
+
File.join(TmpFile.tmproot, 'mongodump')
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Mysqldump < Source
|
6
|
+
|
7
|
+
def command
|
8
|
+
"mysqldump --defaults-extra-file=#{mysql_password_file} #{config[:options]} #{mysql_skip_tables} #{@id}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def extension = '.sql'
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def mysql_password_file
|
16
|
+
WebTranslateIt::Safe::TmpFile.create('mysqldump') do |file|
|
17
|
+
file.puts '[mysqldump]'
|
18
|
+
%w[user password socket host port].each do |k|
|
19
|
+
v = config[k]
|
20
|
+
# values are quoted if needed
|
21
|
+
file.puts "#{k} = #{v.inspect}" if v
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def mysql_skip_tables
|
27
|
+
return unless (skip_tables = config[:skip_tables])
|
28
|
+
|
29
|
+
[*skip_tables].map { |t| "--ignore-table=#{@id}.#{t}" }.join(' ')
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Pgdump < Source
|
6
|
+
|
7
|
+
def command
|
8
|
+
ENV['PGPASSWORD'] = (config['password'] || nil)
|
9
|
+
"pg_dump #{postgres_options} #{postgres_username} #{postgres_host} #{postgres_port} #{@id}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def extension = '.sql'
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def postgres_options
|
17
|
+
config[:options]
|
18
|
+
end
|
19
|
+
|
20
|
+
def postgres_host
|
21
|
+
config['host'] && "--host='#{config['host']}'"
|
22
|
+
end
|
23
|
+
|
24
|
+
def postgres_port
|
25
|
+
config['port'] && "--port='#{config['port']}'"
|
26
|
+
end
|
27
|
+
|
28
|
+
def postgres_username
|
29
|
+
config['user'] && "--username='#{config['user']}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Pipe < Stream
|
6
|
+
|
7
|
+
# process adds required commands to the current
|
8
|
+
# shell command string
|
9
|
+
# :active?, :pipe, :extension and :post_process are
|
10
|
+
# defined in inheriting pipe classes
|
11
|
+
def process
|
12
|
+
return unless active?
|
13
|
+
|
14
|
+
@backup.command << pipe
|
15
|
+
@backup.extension << extension
|
16
|
+
post_process
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
module Safe
|
3
|
+
class S3 < Sink
|
4
|
+
MAX_S3_FILE_SIZE = 5368709120
|
5
|
+
|
6
|
+
def active?
|
7
|
+
bucket && key && secret
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def path
|
13
|
+
@path ||= expand(config[:s3, :path] || config[:local, :path] || ':kind/:id')
|
14
|
+
end
|
15
|
+
|
16
|
+
def save
|
17
|
+
# FIXME: user friendly error here :)
|
18
|
+
raise RuntimeError, 'pipe-streaming not supported for S3.' unless @backup.path
|
19
|
+
|
20
|
+
# needed in cleanup even on dry run
|
21
|
+
AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true) unless local_only?
|
22
|
+
|
23
|
+
puts "Uploading #{bucket}:#{full_path}" if verbose? || dry_run?
|
24
|
+
unless dry_run? || local_only?
|
25
|
+
if File.stat(@backup.path).size > MAX_S3_FILE_SIZE
|
26
|
+
STDERR.puts "ERROR: File size exceeds maximum allowed for upload to S3 (#{MAX_S3_FILE_SIZE}): #{@backup.path}"
|
27
|
+
return
|
28
|
+
end
|
29
|
+
benchmark = Benchmark.realtime do
|
30
|
+
AWS::S3::Bucket.create(bucket) unless bucket_exists?(bucket)
|
31
|
+
File.open(@backup.path) do |file|
|
32
|
+
AWS::S3::S3Object.store(full_path, file, bucket)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
puts '...done' if verbose?
|
36
|
+
puts('Upload took ' + sprintf('%.2f', benchmark) + ' second(s).') if verbose?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def cleanup
|
41
|
+
return if local_only?
|
42
|
+
|
43
|
+
return unless keep = config[:keep, :s3]
|
44
|
+
|
45
|
+
puts "listing files: #{bucket}:#{base}*" if verbose?
|
46
|
+
files = AWS::S3::Bucket.objects(bucket, :prefix => base, :max_keys => keep * 2)
|
47
|
+
puts files.collect {|x| x.key} if verbose?
|
48
|
+
|
49
|
+
files = files.
|
50
|
+
collect {|x| x.key}.
|
51
|
+
sort
|
52
|
+
|
53
|
+
cleanup_with_limit(files, keep) do |f|
|
54
|
+
puts "removing s3 file #{bucket}:#{f}" if dry_run? || verbose?
|
55
|
+
AWS::S3::Bucket.objects(bucket, :prefix => f)[0].delete unless dry_run? || local_only?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def bucket
|
60
|
+
config[:s3, :bucket]
|
61
|
+
end
|
62
|
+
|
63
|
+
def key
|
64
|
+
config[:s3, :key]
|
65
|
+
end
|
66
|
+
|
67
|
+
def secret
|
68
|
+
config[:s3, :secret]
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def bucket_exists?(bucket)
|
74
|
+
true if AWS::S3::Bucket.find(bucket)
|
75
|
+
rescue AWS::S3::NoSuchBucket
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Sftp < Sink
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
def active?
|
10
|
+
host && user
|
11
|
+
end
|
12
|
+
|
13
|
+
def path
|
14
|
+
@path ||= expand(config[:sftp, :path] || config[:local, :path] || ':kind/:id')
|
15
|
+
end
|
16
|
+
|
17
|
+
def save
|
18
|
+
raise 'pipe-streaming not supported for SFTP.' unless @backup.path
|
19
|
+
|
20
|
+
puts "Uploading #{host}:#{full_path} via SFTP" if verbose? || dry_run?
|
21
|
+
|
22
|
+
return if dry_run? || local_only?
|
23
|
+
|
24
|
+
opts = {}
|
25
|
+
opts[:password] = password if password
|
26
|
+
opts[:port] = port if port
|
27
|
+
Net::SFTP.start(host, user, opts) do |sftp|
|
28
|
+
puts "Sending #{@backup.path} to #{full_path}" if verbose?
|
29
|
+
begin
|
30
|
+
sftp.upload! @backup.path, full_path
|
31
|
+
rescue Net::SFTP::StatusException
|
32
|
+
puts "Ensuring remote path (#{path}) exists" if verbose?
|
33
|
+
# mkdir -p
|
34
|
+
folders = path.split('/')
|
35
|
+
folders.each_index do |i|
|
36
|
+
folder = folders[0..i].join('/')
|
37
|
+
puts "Creating #{folder} on remote" if verbose?
|
38
|
+
begin
|
39
|
+
sftp.mkdir!(folder)
|
40
|
+
rescue StandardError
|
41
|
+
Net::SFTP::StatusException
|
42
|
+
end
|
43
|
+
end
|
44
|
+
retry
|
45
|
+
end
|
46
|
+
end
|
47
|
+
puts '...done' if verbose?
|
48
|
+
end
|
49
|
+
|
50
|
+
def cleanup
|
51
|
+
return if local_only? || dry_run?
|
52
|
+
|
53
|
+
return unless (keep = config[:keep, :sftp])
|
54
|
+
|
55
|
+
puts "listing files: #{host}:#{base}*" if verbose?
|
56
|
+
opts = {}
|
57
|
+
opts[:password] = password if password
|
58
|
+
opts[:port] = port if port
|
59
|
+
Net::SFTP.start(host, user, opts) do |sftp|
|
60
|
+
files = sftp.dir.glob(path, File.basename("#{base}*"))
|
61
|
+
|
62
|
+
puts(files.collect(&:name)) if verbose?
|
63
|
+
|
64
|
+
files = files
|
65
|
+
.collect(&:name)
|
66
|
+
.sort
|
67
|
+
|
68
|
+
cleanup_with_limit(files, keep) do |f|
|
69
|
+
file = File.join(path, f)
|
70
|
+
puts "removing sftp file #{host}:#{file}" if dry_run? || verbose?
|
71
|
+
sftp.remove!(file) unless dry_run? || local_only?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def host
|
77
|
+
config[:sftp, :host]
|
78
|
+
end
|
79
|
+
|
80
|
+
def user
|
81
|
+
config[:sftp, :user]
|
82
|
+
end
|
83
|
+
|
84
|
+
def password
|
85
|
+
config[:sftp, :password]
|
86
|
+
end
|
87
|
+
|
88
|
+
def port
|
89
|
+
config[:sftp, :port]
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Sink < Stream
|
6
|
+
|
7
|
+
def process
|
8
|
+
return unless active?
|
9
|
+
|
10
|
+
save
|
11
|
+
cleanup
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
# path is defined in subclass
|
17
|
+
# base is used in 'cleanup' to find all files that begin with base. the '.'
|
18
|
+
# at the end is essential to distinguish b/w foo.* and foobar.* archives for example
|
19
|
+
def base
|
20
|
+
@base ||= File.join(path, "#{File.basename(@backup.filename).split('.').first}.")
|
21
|
+
end
|
22
|
+
|
23
|
+
def full_path
|
24
|
+
@full_path ||= File.join(path, @backup.filename) + @backup.extension
|
25
|
+
end
|
26
|
+
|
27
|
+
# call block on files to be removed (all except for the LAST 'limit' files
|
28
|
+
def cleanup_with_limit(files, limit, &)
|
29
|
+
return unless files.size > limit
|
30
|
+
|
31
|
+
to_remove = files[0..(files.size - limit - 1)]
|
32
|
+
# TODO: validate here
|
33
|
+
to_remove.each(&)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module WebTranslateIt
|
2
|
+
|
3
|
+
module Safe
|
4
|
+
|
5
|
+
class Source < Stream
|
6
|
+
|
7
|
+
attr_accessor :id
|
8
|
+
|
9
|
+
def initialize(id, config)
|
10
|
+
@id = id.to_s
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
def timestamp
|
15
|
+
Time.now.strftime('%y%m%d-%H%M')
|
16
|
+
end
|
17
|
+
|
18
|
+
def kind
|
19
|
+
self.class.human_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def filename
|
23
|
+
@filename ||= expand(':kind-:id.:timestamp')
|
24
|
+
end
|
25
|
+
|
26
|
+
def backup
|
27
|
+
return @backup if @backup
|
28
|
+
|
29
|
+
@backup = Backup.new(
|
30
|
+
id: @id,
|
31
|
+
kind: kind,
|
32
|
+
extension: extension,
|
33
|
+
command: command,
|
34
|
+
timestamp: timestamp
|
35
|
+
)
|
36
|
+
# can't do this in the initializer hash above since
|
37
|
+
# filename() calls expand() which requires @backup
|
38
|
+
# FIXME: move expansion to the backup (last step in ctor) assign :tags here
|
39
|
+
@backup.filename = filename
|
40
|
+
@backup
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.human_name
|
44
|
+
name.split('::').last.downcase
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|