synco 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +4 -0
- data/.simplecov +9 -0
- data/.travis.yml +14 -0
- data/Gemfile +11 -0
- data/README.md +246 -0
- data/Rakefile +8 -0
- data/bin/synco +30 -0
- data/lib/synco.rb +51 -0
- data/lib/synco/command.rb +71 -0
- data/lib/synco/command/disk.rb +55 -0
- data/lib/synco/command/prune.rb +166 -0
- data/lib/synco/command/rotate.rb +86 -0
- data/lib/synco/command/spawn.rb +39 -0
- data/lib/synco/compact_formatter.rb +115 -0
- data/lib/synco/controller.rb +119 -0
- data/lib/synco/directory.rb +60 -0
- data/lib/synco/disk.rb +68 -0
- data/lib/synco/method.rb +56 -0
- data/lib/synco/methods/rsync.rb +162 -0
- data/lib/synco/methods/scp.rb +44 -0
- data/lib/synco/methods/zfs.rb +60 -0
- data/lib/synco/scope.rb +247 -0
- data/lib/synco/script.rb +128 -0
- data/lib/synco/server.rb +90 -0
- data/lib/synco/shell.rb +44 -0
- data/lib/synco/shells/ssh.rb +52 -0
- data/lib/synco/version.rb +23 -0
- data/media/LSync Logo.artx/Preview/preview.png +0 -0
- data/media/LSync Logo.artx/QuickLook/Preview.pdf +0 -0
- data/media/LSync Logo.artx/doc.thread +0 -0
- data/media/LSync Logo.png +0 -0
- data/spec/synco/backup_script.rb +63 -0
- data/spec/synco/directory_spec.rb +33 -0
- data/spec/synco/local_backup.rb +56 -0
- data/spec/synco/local_sync.rb +91 -0
- data/spec/synco/method_spec.rb +62 -0
- data/spec/synco/rsync_spec.rb +89 -0
- data/spec/synco/scp_spec.rb +58 -0
- data/spec/synco/script_spec.rb +51 -0
- data/spec/synco/shell_spec.rb +42 -0
- data/spec/synco/usb_spec.rb +76 -0
- data/spec/synco/zfs_spec.rb +50 -0
- data/synco.gemspec +35 -0
- metadata +254 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
# This script takes a given path, and renames it with the given format.
|
22
|
+
# It then ensures that there is a symlink called "latest" that points
|
23
|
+
# to the renamed directory.
|
24
|
+
|
25
|
+
require 'samovar'
|
26
|
+
|
27
|
+
require_relative '../disk'
|
28
|
+
|
29
|
+
module Synco
|
30
|
+
module Command
|
31
|
+
class Mount < Samovar::Command
|
32
|
+
self.description = "Mount a disk with the given name."
|
33
|
+
|
34
|
+
one :path, "The disk mount point."
|
35
|
+
one :name, "The symbolic name of the disk to mount, e.g. disk label."
|
36
|
+
|
37
|
+
def invoke(parent)
|
38
|
+
# We may not have permission to make this directory, but we should still try:
|
39
|
+
FileUtils.mkpath(@path) rescue nil
|
40
|
+
|
41
|
+
Disk.mount(@path, @name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Unmount < Samovar::Command
|
46
|
+
self.description = "Unmount a disk with the given name."
|
47
|
+
|
48
|
+
one :path, "The disk mount point."
|
49
|
+
|
50
|
+
def invoke(parent)
|
51
|
+
Disk.unmount(@path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
# This script takes a given path, and renames it with the given format.
|
22
|
+
# It then ensures that there is a symlink called "latest" that points
|
23
|
+
# to the renamed directory.
|
24
|
+
|
25
|
+
require 'samovar'
|
26
|
+
|
27
|
+
require 'periodical/filter'
|
28
|
+
|
29
|
+
# Required for strptime
|
30
|
+
require 'date'
|
31
|
+
|
32
|
+
module Synco
|
33
|
+
module Command
|
34
|
+
class Rotation
|
35
|
+
include Comparable
|
36
|
+
|
37
|
+
def initialize(path, time)
|
38
|
+
@path = path
|
39
|
+
@time = time
|
40
|
+
end
|
41
|
+
|
42
|
+
attr :time
|
43
|
+
attr :path
|
44
|
+
|
45
|
+
# Sort in reverse order by default
|
46
|
+
def <=> other
|
47
|
+
other.time <=> @time
|
48
|
+
end
|
49
|
+
|
50
|
+
def eql? other
|
51
|
+
case other
|
52
|
+
when Rotation
|
53
|
+
@path.eql?(other.path)
|
54
|
+
else
|
55
|
+
@path.eql?(other.to_s)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def hash
|
60
|
+
@path.hash
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
@path
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Prune < Samovar::Command
|
69
|
+
self.description = "Prune old backups to reduce disk usage according to a given policy."
|
70
|
+
|
71
|
+
options do
|
72
|
+
option "--hourly <count>", "Set the number of hourly backups to keep.", default: 24
|
73
|
+
option "--daily <count>", "Set the number of daily backups to keep.", default: 7*4
|
74
|
+
option "--weekly <count>", "Set the number of weekly backups to keep.", default: 52
|
75
|
+
option "--monthly <count>", "Set the number of monthly backups to keep.", default: 12*3
|
76
|
+
option "--quarterly <count>", "Set the number of quaterly backups to keep.", default: 4*10
|
77
|
+
option "--yearly <count>", "Set the number of yearly backups to keep.", default: 20
|
78
|
+
|
79
|
+
option "--format <name>", "Set the name of the backup rotations, including strftime expansions.", default: BACKUP_NAME
|
80
|
+
option "--latest <name>", "The name of the latest backup symlink.", default: LATEST_NAME
|
81
|
+
|
82
|
+
option "--keep <new|old>", "Keep the younger or older backups within the same period division", default: 'old'
|
83
|
+
|
84
|
+
option "--dry", "Print out what would be done rather than doing it."
|
85
|
+
end
|
86
|
+
|
87
|
+
def policy
|
88
|
+
policy = Periodical::Filter::Policy.new
|
89
|
+
|
90
|
+
policy << Periodical::Filter::Hourly.new(@options[:hourly])
|
91
|
+
policy << Periodical::Filter::Daily.new(@options[:daily])
|
92
|
+
policy << Periodical::Filter::Weekly.new(@options[:weekly])
|
93
|
+
policy << Periodical::Filter::Monthly.new(@options[:monthly])
|
94
|
+
policy << Periodical::Filter::Quarterly.new(@options[:quarterly])
|
95
|
+
policy << Periodical::Filter::Yearly.new(@options[:yearly])
|
96
|
+
|
97
|
+
return policy
|
98
|
+
end
|
99
|
+
|
100
|
+
def current_backups
|
101
|
+
backups = []
|
102
|
+
|
103
|
+
Dir['*'].each do |path|
|
104
|
+
next if path == @options[:latest]
|
105
|
+
date_string = File.basename(path)
|
106
|
+
|
107
|
+
begin
|
108
|
+
backups << Rotation.new(path, DateTime.strptime(date_string, @options[:format]))
|
109
|
+
rescue ArgumentError
|
110
|
+
$stderr.puts "Skipping #{path}, error parsing #{date_string}: #{$!}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
return backups
|
115
|
+
end
|
116
|
+
|
117
|
+
def dry?
|
118
|
+
@options[:dry]
|
119
|
+
end
|
120
|
+
|
121
|
+
def print_rotation(keep, erase)
|
122
|
+
puts "*** Rotating backups (DRY!) ***"
|
123
|
+
puts "\tKeeping:"
|
124
|
+
keep.sort.each { |backup| puts "\t\t#{backup.path}" }
|
125
|
+
puts "\tErasing:"
|
126
|
+
erase.sort.each { |backup| puts "\t\t#{backup.path}" }
|
127
|
+
end
|
128
|
+
|
129
|
+
def perform_rotation(keep, erase)
|
130
|
+
puts "*** Rotating backups ***"
|
131
|
+
erase.sort.each do |backup|
|
132
|
+
puts "Erasing #{backup.path}..."
|
133
|
+
$stdout.flush
|
134
|
+
|
135
|
+
# Ensure that we can remove the backup
|
136
|
+
system("chmod", "-R", "ug+rwX", backup.path)
|
137
|
+
system("rm", "-rf", backup.path)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def invoke(parent)
|
142
|
+
backups = current_backups
|
143
|
+
|
144
|
+
retain, erase = policy.filter(backups, keep: @options[:keep].to_sym, &:time)
|
145
|
+
|
146
|
+
# We need to retain the latest backup regardless of policy
|
147
|
+
if latest = @options[:latest] and File.exist?(latest)
|
148
|
+
latest_path = File.readlink(options[:latest])
|
149
|
+
latest_rotation = erase.find{|rotation| rotation.path == latest_path}
|
150
|
+
|
151
|
+
if latest_rotation
|
152
|
+
puts "Retaining latest backup #{latest_rotation}"
|
153
|
+
erase.delete(latest_rotation)
|
154
|
+
retain << latest_rotation
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
if dry?
|
159
|
+
print_rotation(retain, erase)
|
160
|
+
else
|
161
|
+
perform_rotation(retain, erase)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
# This script takes a given path, and renames it with the given format.
|
22
|
+
# It then ensures that there is a symlink called "latest" that points
|
23
|
+
# to the renamed directory.
|
24
|
+
|
25
|
+
require 'samovar'
|
26
|
+
|
27
|
+
require_relative '../method'
|
28
|
+
|
29
|
+
module Synco
|
30
|
+
module Command
|
31
|
+
class Rotate < Samovar::Command
|
32
|
+
self.description = "Rotate a backup snapshot into a timestamped directory."
|
33
|
+
|
34
|
+
options do
|
35
|
+
option "--format <name>", "Set the name of the backup rotations, including strftime expansions.", default: BACKUP_NAME
|
36
|
+
option "--latest <name>", "The name of the latest backup symlink.", default: LATEST_NAME
|
37
|
+
option "--snapshot <name>", "The name of the in-progress backup snapshot.", default: SNAPSHOT_NAME
|
38
|
+
|
39
|
+
# option "--timezone <name>", "The default timezone for backup timestamps.", default: BACKUP_TIMEZONE
|
40
|
+
end
|
41
|
+
|
42
|
+
def backup_timestamp
|
43
|
+
timestamp = Time.now.utc
|
44
|
+
|
45
|
+
#if timezone = @options[:timezone]
|
46
|
+
# timestamp = timestamp.in_time_zone(timezone)
|
47
|
+
#end
|
48
|
+
|
49
|
+
return timestamp
|
50
|
+
end
|
51
|
+
|
52
|
+
def backup_name
|
53
|
+
backup_timestamp.strftime(@options[:format])
|
54
|
+
end
|
55
|
+
|
56
|
+
def invoke(parent)
|
57
|
+
snapshot_name = @options[:snapshot]
|
58
|
+
unless File.exist? snapshot_name
|
59
|
+
$stderr.puts "Snapshot directory #{snapshot_name} does not exist!"
|
60
|
+
exit(10)
|
61
|
+
end
|
62
|
+
|
63
|
+
rotated_name = backup_name
|
64
|
+
if File.exist? rotated_name
|
65
|
+
$stderr.puts "Destination rotation name #{rotated_name} already exists!"
|
66
|
+
exit(20)
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "Rotating #{snapshot_name} to #{rotated_name} in #{Dir.pwd}"
|
70
|
+
|
71
|
+
# Move rotated dir
|
72
|
+
FileUtils.mv(snapshot_name, rotated_name)
|
73
|
+
|
74
|
+
# Recreate latest symlink
|
75
|
+
latest_link = @options[:latest]
|
76
|
+
if File.symlink?(latest_link)
|
77
|
+
puts "Removing old latest link..."
|
78
|
+
FileUtils.rm(latest_link)
|
79
|
+
end
|
80
|
+
|
81
|
+
puts "Creating latest symlink to #{rotated_name}"
|
82
|
+
FileUtils.ln_s(rotated_name, latest_link)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
# This script takes a given path, and renames it with the given format.
|
22
|
+
# It then ensures that there is a symlink called "latest" that points
|
23
|
+
# to the renamed directory.
|
24
|
+
|
25
|
+
require 'samovar'
|
26
|
+
|
27
|
+
module Synco
|
28
|
+
module Command
|
29
|
+
class Spawn < Samovar::Command
|
30
|
+
self.description = "Run a command using the synco environment and root directory."
|
31
|
+
|
32
|
+
split :argv, "Command to spawn."
|
33
|
+
|
34
|
+
def invoke(parent)
|
35
|
+
Process.exec(*@argv)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rainbow'
|
22
|
+
require 'shellwords'
|
23
|
+
|
24
|
+
module Synco
|
25
|
+
class CompactFormatter
|
26
|
+
def initialize(verbose: true)
|
27
|
+
@start = Time.now
|
28
|
+
@verbose = verbose
|
29
|
+
end
|
30
|
+
|
31
|
+
def time_offset_prefix
|
32
|
+
offset = Time.now - @start
|
33
|
+
minutes = (offset/60).floor
|
34
|
+
seconds = (offset - (minutes*60))
|
35
|
+
|
36
|
+
if minutes > 0
|
37
|
+
"#{minutes}m#{seconds.floor}s"
|
38
|
+
else
|
39
|
+
"#{seconds.round(2)}s"
|
40
|
+
end.rjust(6)
|
41
|
+
end
|
42
|
+
|
43
|
+
def chdir_string(options)
|
44
|
+
if options[:chdir]
|
45
|
+
" in #{options[:chdir]}"
|
46
|
+
else
|
47
|
+
""
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def format_command(arguments, buffer)
|
52
|
+
arguments = arguments.dup
|
53
|
+
|
54
|
+
# environment = arguments.first.is_a?(Hash) ? arguments.shift : nil
|
55
|
+
options = arguments.last.is_a?(Hash) ? arguments.pop : nil
|
56
|
+
|
57
|
+
arguments = arguments.flatten.collect(&:to_s)
|
58
|
+
|
59
|
+
buffer << Rainbow(arguments.shelljoin).bright.blue
|
60
|
+
|
61
|
+
if options
|
62
|
+
buffer << chdir_string(options)
|
63
|
+
end
|
64
|
+
|
65
|
+
buffer << "\n"
|
66
|
+
|
67
|
+
# if environment
|
68
|
+
# environment.each do |key,value|
|
69
|
+
# buffer << "\texport #{key}=#{value.dump}\n"
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
end
|
73
|
+
|
74
|
+
def format_exception(exception, buffer)
|
75
|
+
buffer << Rainbow("#{exception.class}: #{exception}").bright.red << "\n"
|
76
|
+
exception.backtrace.each do |line|
|
77
|
+
buffer << "\t" << Rainbow(line).red << "\n"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def format_line(message, severity)
|
82
|
+
if severity == 'ERROR'
|
83
|
+
Rainbow(message).red
|
84
|
+
else
|
85
|
+
message
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def call(severity, datetime, progname, message)
|
90
|
+
buffer = []
|
91
|
+
prefix = ""
|
92
|
+
|
93
|
+
if @verbose
|
94
|
+
prefix = time_offset_prefix
|
95
|
+
buffer << Rainbow(prefix).cyan + ": "
|
96
|
+
prefix = " " * (prefix.size) + "| "
|
97
|
+
end
|
98
|
+
|
99
|
+
if progname == 'shell' and message.kind_of? Array
|
100
|
+
format_command(message, buffer)
|
101
|
+
elsif message.kind_of? Exception
|
102
|
+
format_exception(message, buffer)
|
103
|
+
else
|
104
|
+
buffer << format_line(message, severity) << "\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
result = buffer.join
|
108
|
+
|
109
|
+
# This fancy regex indents lines correctly depending on the prefix:
|
110
|
+
result.gsub!(/\n(?!$)/, "\n#{prefix}") unless prefix.empty?
|
111
|
+
|
112
|
+
return result
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|