s3-tar-backup 1.1.2 → 1.1.3
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 +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
|