s3-tar-backup 1.1.2 → 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +28 -6
- data/lib/s3_tar_backup.rb +346 -312
- data/lib/s3_tar_backup/backend/backend_object.rb +3 -0
- data/lib/s3_tar_backup/backend/file_backend.rb +52 -0
- data/lib/s3_tar_backup/backend/s3_backend.rb +62 -0
- data/lib/s3_tar_backup/backend/upload_item_failed_error.rb +4 -0
- data/lib/s3_tar_backup/backup.rb +115 -88
- data/lib/s3_tar_backup/ini_parser.rb +206 -205
- data/lib/s3_tar_backup/version.rb +1 -1
- metadata +11 -7
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
require_relative 'upload_item_failed_error'
|
5
|
+
require_relative 'backend_object'
|
6
|
+
|
7
|
+
module S3TarBackup::Backend
|
8
|
+
class FileBackend
|
9
|
+
attr_reader :prefix
|
10
|
+
|
11
|
+
def initialize(path)
|
12
|
+
@prefix = Pathname.new(path).absolute? ? path : File.expand_path(File.join('~', path))
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
"File"
|
17
|
+
end
|
18
|
+
|
19
|
+
def remove_item(relative_path)
|
20
|
+
File.delete(File.join(@prefix, relative_path))
|
21
|
+
end
|
22
|
+
|
23
|
+
def item_exists?(relative_path)
|
24
|
+
File.exists?(File.join(@prefix, relative_path))
|
25
|
+
end
|
26
|
+
|
27
|
+
def download_item(relative_path, local_path)
|
28
|
+
FileUtils.cp(File.join(@prefix, relative_path), local_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def upload_item(relative_path, local_path, remove_original)
|
32
|
+
path = File.join(@prefix, relative_path)
|
33
|
+
FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
|
34
|
+
if remove_original
|
35
|
+
FileUtils.mv(local_path, path)
|
36
|
+
else
|
37
|
+
FileUtils.cp(local_path, path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def list_items(relative_path='')
|
42
|
+
return [] unless File.directory?(File.join(@prefix, relative_path))
|
43
|
+
relative_path = '.' if relative_path.nil? || relative_path.empty?
|
44
|
+
Dir.chdir(@prefix) do
|
45
|
+
Dir.entries(relative_path).select{ |x| File.file?(x) }.map do |x|
|
46
|
+
path = File.join(relative_path, x)
|
47
|
+
BackendObject.new(path, File.size?(path))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
require_relative 'upload_item_failed_error'
|
4
|
+
require_relative 'backend_object'
|
5
|
+
|
6
|
+
module S3TarBackup::Backend
|
7
|
+
class S3Backend
|
8
|
+
attr_reader :prefix
|
9
|
+
|
10
|
+
@s3
|
11
|
+
|
12
|
+
def initialize(access_key, secret_key, region, dest_prefix)
|
13
|
+
warn "No AWS region specified (config key settings.s3_region). Assuming eu-west-1" unless region
|
14
|
+
@s3 = AWS::S3.new(access_key_id: access_key, secret_access_key: secret_key, region: region || 'eu-west-1')
|
15
|
+
@prefix = dest_prefix
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
"S3"
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_item(relative_path)
|
23
|
+
bucket, path = parse_bucket_object("#{@prefix}/#{relative_path}")
|
24
|
+
@s3.buckets[bucket].objects[path].delete
|
25
|
+
end
|
26
|
+
|
27
|
+
def item_exists?(relative_path)
|
28
|
+
bucket, path = parse_bucket_object("#{@prefix}/#{relative_path}")
|
29
|
+
@s3.buckets[bucket].objects[path].exists?
|
30
|
+
end
|
31
|
+
|
32
|
+
def download_item(relative_path, local_path)
|
33
|
+
bucket, path = parse_bucket_object("#{@prefix}/#{relative_path}")
|
34
|
+
object = @s3.buckets[bucket].objects[path]
|
35
|
+
open(local_path, 'wb') do |f|
|
36
|
+
object.read do |chunk|
|
37
|
+
f.write(chunk)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def upload_item(relative_path, local_path, remove_original)
|
43
|
+
bucket, path = parse_bucket_object("#{@prefix}/#{relative_path}")
|
44
|
+
@s3.buckets[bucket].objects.create(path, Pathname.new(local_path))
|
45
|
+
File.delete(local_path) if remove_original
|
46
|
+
rescue Errno::ECONNRESET => e
|
47
|
+
raise UploadItemFailedError.new, e.message
|
48
|
+
end
|
49
|
+
|
50
|
+
def list_items(relative_path='')
|
51
|
+
bucket, path = parse_bucket_object("#{@prefix}/#{relative_path}")
|
52
|
+
@s3.buckets[bucket].objects.with_prefix(path).map do |x|
|
53
|
+
BackendObject.new(x.key, x.content_length)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def parse_bucket_object(path)
|
59
|
+
path.split('/', 2)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/s3_tar_backup/backup.rb
CHANGED
@@ -1,91 +1,118 @@
|
|
1
|
-
require '
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
2
3
|
|
3
4
|
module S3TarBackup
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
5
|
+
class Backup
|
6
|
+
@backup_dir
|
7
|
+
@name
|
8
|
+
@sources
|
9
|
+
@exclude
|
10
|
+
@time # Here to avoid any sort of race conditions
|
11
|
+
@archive
|
12
|
+
@compression_flag
|
13
|
+
@compression_ext
|
14
|
+
@encryption
|
15
|
+
|
16
|
+
COMPRESSIONS = {
|
17
|
+
:none => {:flag => '', :ext => 'tar'},
|
18
|
+
:gzip => {:flag => '-z', :ext => 'tar.gz'},
|
19
|
+
:bzip2 => {:flag => '-j', :ext => 'tar.bz2'},
|
20
|
+
:lzma => {:flag => '--lzma', :ext => 'tar.lzma'},
|
21
|
+
:lzma2 => {:flag => '-J', :ext => 'tar.xz'}
|
22
|
+
}
|
23
|
+
|
24
|
+
ENCRYPTED_EXTENSIONS = { :gpg_key => 'asc', :password_file => 'gpg' }
|
25
|
+
PASSPHRASE_CIPHER_ALGO = 'AES256'
|
26
|
+
|
27
|
+
|
28
|
+
def initialize(backup_dir, name, sources, exclude, compression=:bzip2, encryption=nil)
|
29
|
+
@backup_dir, @name = backup_dir, name
|
30
|
+
raise "Unknown compression #{compression}. Valid options are #{COMPRESSIONS.keys.join(', ')}" unless COMPRESSIONS.has_key?(compression)
|
31
|
+
@compression_flag = COMPRESSIONS[compression][:flag]
|
32
|
+
@compression_ext = COMPRESSIONS[compression][:ext]
|
33
|
+
@time = Time.now
|
34
|
+
@encryption = encryption
|
35
|
+
|
36
|
+
@sources = [*sources].map{ |x| x.gsub('\\', '/') }
|
37
|
+
@exclude = [*exclude].map{ |x| x.gsub('\\', '/') }
|
38
|
+
|
39
|
+
# If the backup dir is inside any of the sources, exclude it
|
40
|
+
absolute_backup_dir = File.absolute_path(backup_dir)
|
41
|
+
# I cannot for the life of me get tar to accept absolute paths to directories. Passing a path relative to the CWD seems to work though
|
42
|
+
@exclude.push(*@sources.select{ |x| absolute_backup_dir.start_with?(File.absolute_path(x)) }.map{ |x| Pathname.new(absolute_backup_dir).relative_path_from(Pathname.new(Dir.pwd)).to_s })
|
43
|
+
|
44
|
+
FileUtils.mkdir_p(@backup_dir) unless File.directory?(@backup_dir)
|
45
|
+
end
|
46
|
+
|
47
|
+
def snar
|
48
|
+
"backup-#{@name}.snar"
|
49
|
+
end
|
50
|
+
|
51
|
+
def snar_path
|
52
|
+
File.join(@backup_dir, snar)
|
53
|
+
end
|
54
|
+
|
55
|
+
def tmp_snar_path
|
56
|
+
snar_path + '.tmp'
|
57
|
+
end
|
58
|
+
|
59
|
+
def snar_exists?
|
60
|
+
File.exists?(snar_path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def archive
|
64
|
+
return @archive if @archive
|
65
|
+
type = snar_exists? ? 'incr' : 'full'
|
66
|
+
encrypted_bit = @encryption ? ".#{ENCRYPTED_EXTENSIONS[@encryption[:type]]}" : ''
|
67
|
+
File.join(@backup_dir, "backup-#{@name}-#{@time.strftime('%Y%m%d_%H%M%S')}-#{type}.#{@compression_ext}#{encrypted_bit}")
|
68
|
+
end
|
69
|
+
|
70
|
+
def backup_cmd(verbose=false)
|
71
|
+
exclude = @exclude.map{ |e| " --exclude \"#{e}\""}.join
|
72
|
+
sources = @sources.map{ |s| "\"#{s}\""}.join(' ')
|
73
|
+
@archive = archive
|
74
|
+
tar_archive = @encryption ? '' : "f \"#{@archive}\""
|
75
|
+
gpg_cmd = @encryption.nil? ? '' : case @encryption[:type]
|
76
|
+
when :gpg_key
|
77
|
+
" | gpg -r #{@encryption[:gpg_key]} -o \"#{@archive}\" --always-trust --yes --batch --no-tty -e"
|
78
|
+
when :password_file
|
79
|
+
" | gpg -c --passphrase-file \"#{@encryption[:password_file]}\" --cipher-algo #{PASSPHRASE_CIPHER_ALGO} -o \"#{@archive}\" --batch --yes --no-tty"
|
80
|
+
end
|
81
|
+
"tar c#{verbose ? 'v' : ''}#{tar_archive} #{@compression_flag} -g \"#{tmp_snar_path}\"#{exclude} --no-check-device #{sources}#{gpg_cmd}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.parse_object(object, profile)
|
85
|
+
name = File.basename(object.path)
|
86
|
+
match = name.match(/^backup-([\w\-]+)-(\d\d\d\d)(\d\d)(\d\d)_(\d\d)(\d\d)(\d\d)-(\w+)\.(.*?)(?:\.(#{ENCRYPTED_EXTENSIONS.values.join('|')}))?$/)
|
87
|
+
return nil unless match && match[1] == profile
|
88
|
+
|
89
|
+
return {
|
90
|
+
:type => match[8].to_sym,
|
91
|
+
:date => Time.new(match[2].to_i, match[3].to_i, match[4].to_i, match[5].to_i, match[6].to_i, match[7].to_i),
|
92
|
+
:name => name,
|
93
|
+
:ext => match[9],
|
94
|
+
:size => object.size,
|
95
|
+
:profile => match[1],
|
96
|
+
:compression => COMPRESSIONS.find{ |k,v| v[:ext] == match[9] }[0],
|
97
|
+
:encryption => match[10].nil? ? nil : ENCRYPTED_EXTENSIONS.key(match[10])
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
# No real point in creating a whole new class for this one
|
102
|
+
def self.restore_cmd(restore_into, restore_from, verbose=false, password_file=nil)
|
103
|
+
ext, encryption_ext = restore_from.match(/[^\.\\\/]+\.(.*?)(?:\.(#{ENCRYPTED_EXTENSIONS.values.join('|')}))?$/)[1..2]
|
104
|
+
encryption = ENCRYPTED_EXTENSIONS.key(encryption_ext)
|
105
|
+
compression_flag = COMPRESSIONS.find{ |k,v| v[:ext] == ext }[1][:flag]
|
106
|
+
tar_archive = encryption ? '' : "f \"#{restore_from}\""
|
107
|
+
gpg_cmd = encryption.nil? ? '' : case encryption
|
108
|
+
when :gpg_key
|
109
|
+
"gpg --yes -d \"#{restore_from}\" | "
|
110
|
+
when :password_file
|
111
|
+
flag = password_file && !password_file.empty? ? " --passphrase-file \"#{password_file}\"" : ''
|
112
|
+
"gpg --yes#{flag} -d \"#{restore_from}\" | "
|
113
|
+
end
|
114
|
+
"#{gpg_cmd}tar xp#{verbose ? 'v' : ''}#{tar_archive} #{compression_flag} -G -C #{restore_into}"
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
91
118
|
end
|
@@ -1,207 +1,208 @@
|
|
1
1
|
module S3TarBackup
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
2
|
+
class IniParser
|
3
|
+
attr_reader :file_path
|
4
|
+
|
5
|
+
@config
|
6
|
+
@comments
|
7
|
+
@defaults
|
8
|
+
|
9
|
+
def initialize(file_path, defaults={})
|
10
|
+
@file_path, @defaults = file_path, defaults
|
11
|
+
end
|
12
|
+
|
13
|
+
# Loads the config from file, and parses it
|
14
|
+
def load
|
15
|
+
if File.exists?(@file_path) && !File.directory?(@file_path)
|
16
|
+
File.open(@file_path) do |f|
|
17
|
+
@config, @comments = parse_config(f.readlines)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
@config, @comments = {}, {}
|
21
|
+
end
|
22
|
+
apply_defaults(@defaults)
|
23
|
+
self # Allow chaining
|
24
|
+
end
|
25
|
+
|
26
|
+
# Saves the config to file
|
27
|
+
def save
|
28
|
+
File.open(@file_path, 'w') do |f|
|
29
|
+
f.write(render_config)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parses a set of lines in a config file, turning them into sections and comments
|
34
|
+
def parse_config(config_lines)
|
35
|
+
# TODO get rid of all the {:before => [], :after => nil}
|
36
|
+
config, comments = {}, {}
|
37
|
+
section = nil
|
38
|
+
next_comment = {:before => [], :after => nil}
|
39
|
+
|
40
|
+
config_lines.each do |line|
|
41
|
+
case line.chomp
|
42
|
+
# Section
|
43
|
+
when /^\[([\w\-]+)(?: "([\w\-]+)")?\]$/
|
44
|
+
section = $1.chomp
|
45
|
+
section << ".#{$2.chomp}" if $2
|
46
|
+
section = section.to_sym
|
47
|
+
config[section] = {} unless config.has_key?(section)
|
48
|
+
comments[section] = {} unless comments.has_key?(section)
|
49
|
+
next_comment = {:before => [], :after => nil}
|
50
|
+
# key line
|
51
|
+
when /^([\w\-]+)\s*=\s*([^;]*?)\s*(?:;\s+(.*))?$/
|
52
|
+
raise "Config key before section" unless section
|
53
|
+
key = $1.chomp.to_sym
|
54
|
+
if config[section].has_key?(key)
|
55
|
+
config[section][key] = [config[section][key]] unless config[section][key].is_a?(Array)
|
56
|
+
config[section][key] << $2.chomp
|
57
|
+
else
|
58
|
+
config[section][key] = $2.chomp
|
59
|
+
end
|
60
|
+
# If we found a comment at the end of the line
|
61
|
+
next_comment[:after] = $3 if $3
|
62
|
+
comments[section][key] = next_comment unless next_comment == {:before => [], :after => nil}
|
63
|
+
next_comment = {:before => [], :after => nil}
|
64
|
+
when /;\s?(.*)/
|
65
|
+
next_comment[:before] << $1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
[config, comments]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Applies the defaults passed to the constructor
|
73
|
+
def apply_defaults(defaults)
|
74
|
+
defaults.each do |key, default|
|
75
|
+
section, key = key.match(/(.*)\.(.*)/)[1..2]
|
76
|
+
|
77
|
+
if default.is_a?(Array)
|
78
|
+
default_val, comment = default
|
79
|
+
else
|
80
|
+
default_val, comment = default, nil
|
81
|
+
end
|
82
|
+
|
83
|
+
@config[section] = {} unless @config.has_key?(section)
|
84
|
+
set("#{section}.#{key}", default_val, comment)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Takes the current config, and renders it
|
89
|
+
def render_config(comments=true)
|
90
|
+
r = ''
|
91
|
+
@config.each do |section_key, section|
|
92
|
+
section_key_parts = section_key.to_s.split('.')
|
93
|
+
if section_key_parts.count > 1
|
94
|
+
r << "\n[#{section_key_parts.shift} \"#{section_key_parts.join(' ')}\"]\n\n"
|
95
|
+
else
|
96
|
+
r << "\n[#{section_key}]\n\n"
|
97
|
+
end
|
98
|
+
section.each do |key, values|
|
99
|
+
values = [*values]
|
100
|
+
comments_before, comments_after = '', ''
|
101
|
+
if comments && @comments.include?(section_key) && @comments[section_key].include?(key)
|
102
|
+
comments_before = @comments[section_key][key][:before].inject(''){ |s,v| s << "; #{v}\n" }
|
103
|
+
comments_after = " ; #{@comments[section_key][key][:after]}" if @comments[section_key][key][:after]
|
104
|
+
end
|
105
|
+
r << comments_before
|
106
|
+
r << values.map{ |value| "#{key} = #{value}" }.join("\n")
|
107
|
+
r << comments_after << "\n\n"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
r.lstrip.rstrip
|
111
|
+
end
|
112
|
+
|
113
|
+
def [](arg)
|
114
|
+
get(arg)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Used to retrieve a config value, with an optional default.
|
118
|
+
# arg: The config key to get, in the form <section>.<key>
|
119
|
+
# default: The value to return if the key doesn't exist.
|
120
|
+
# This function will use type information from self.defaults / default, if available.
|
121
|
+
# Example: config_object.get('section.key', 'default_value')
|
122
|
+
def get(arg, default=nil)
|
123
|
+
section, key = arg.match(/(.*)\.(.*)/)[1..2]
|
124
|
+
section = section.to_sym
|
125
|
+
key = key.to_sym
|
126
|
+
|
127
|
+
unless @config.has_key?(section) && @config[section].has_key?(key)
|
128
|
+
raise "Tried to access config key #{section}.#{key} which doesn't exist" if default.nil?
|
129
|
+
return default
|
130
|
+
end
|
131
|
+
|
132
|
+
val = @config[section][key]
|
133
|
+
# Is it one of the reserved keywords...?
|
134
|
+
case val
|
135
|
+
when 'True' then return true
|
136
|
+
when 'False' then return false
|
137
|
+
when 'None' then return nil
|
138
|
+
end
|
139
|
+
|
140
|
+
# Attempt to case... Is there a default?
|
141
|
+
if default
|
142
|
+
type = default.class
|
143
|
+
elsif @defaults.has_key?("#{section}.#{key}")
|
144
|
+
type = @defaults["#{section}.#{key}"].class
|
145
|
+
# If default is of the form (value, comment)
|
146
|
+
type = @defaults["#{section}.#{key}"][0].class if type.is_a?(Array)
|
147
|
+
else
|
148
|
+
type = nil
|
149
|
+
end
|
150
|
+
|
151
|
+
case type
|
152
|
+
when Integer
|
153
|
+
return val.to_i
|
154
|
+
when Float
|
155
|
+
return val.to_f
|
156
|
+
else
|
157
|
+
return val
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def []=(arg, value)
|
162
|
+
set(arg, value)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Used to set a config value, with optional comments.
|
166
|
+
# arg; The config key to set, in the form <section>.<key>
|
167
|
+
# comments: The comments to set, if any. If multiple lines are desired, they should be separated by "\n"
|
168
|
+
# Example: config_object.set('section.key', 'value', 'This is the comment\nExplaining section.key')
|
169
|
+
def set(arg, value, comments=nil)
|
170
|
+
section, key = arg.match(/(.*)\.(.*)/)[1..2]
|
171
|
+
section = section.to_sym
|
172
|
+
key = key.to_sym
|
173
|
+
|
174
|
+
# Is it one of our special values?
|
175
|
+
case value
|
176
|
+
when true then value = 'True'
|
177
|
+
when false then value = 'False'
|
178
|
+
when nil then value = 'None'
|
179
|
+
end
|
180
|
+
|
181
|
+
@config[section] = {} unless @config.has_key?(section)
|
182
|
+
@config[section][key] = value
|
183
|
+
|
184
|
+
if comments
|
185
|
+
comments = comments.split("\n")
|
186
|
+
@comments[section] = {} unless @comments.has_key?(section)
|
187
|
+
@comments[section][key] = {:before => comments, :after => nil}
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def has_section?(section)
|
192
|
+
@config.has_key?(section.to_sym)
|
193
|
+
end
|
194
|
+
|
195
|
+
def find_sections(pattern=/.*/)
|
196
|
+
@config.select{ |k,v| k =~ pattern }
|
197
|
+
end
|
198
|
+
|
199
|
+
def each
|
200
|
+
@config.each_with_index do |section_key, section|
|
201
|
+
section.each_with_index do |key, value|
|
202
|
+
key_str = "#{section_key}#{key}"
|
203
|
+
yield key_str, get(key_str)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
207
208
|
end
|