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.
@@ -0,0 +1,3 @@
1
+ module S3TarBackup::Backend
2
+ BackendObject = Struct.new(:path, :size)
3
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module S3TarBackup::Backend
2
+ class UploadItemFailedError < StandardError
3
+ end
4
+ end
@@ -1,91 +1,118 @@
1
- require 'aws-sdk'
1
+ require 'fileutils'
2
+ require 'pathname'
2
3
 
3
4
  module S3TarBackup
4
- class Backup
5
- @backup_dir
6
- @name
7
- @sources
8
- @exclude
9
- @time # Here to avoid any sort of race conditions
10
- @archive
11
- @compression_flag
12
- @compression_ext
13
- @gpg_key
14
-
15
- COMPRESSIONS = {
16
- :gzip => {:flag => '-z', :ext => 'tar.gz'},
17
- :bzip2 => {:flag => '-j', :ext => 'tar.bz2'},
18
- :lzma => {:flag => '--lzma', :ext => 'tar.lzma'},
19
- :lzma2 => {:flag => '-J', :ext => 'tar.xz'}
20
- }
21
-
22
- ENCRYPTED_EXTENSION = 'asc'
23
-
24
-
25
- def initialize(backup_dir, name, sources, exclude, compression=:bzip2, gpg_key=nil)
26
- @backup_dir, @name, @sources, @exclude = backup_dir, name, [*sources], [*exclude]
27
- raise "Unknown compression #{compression}. Valid options are #{COMPRESSIONS.keys.join(', ')}" unless COMPRESSIONS.has_key?(compression)
28
- @compression_flag = COMPRESSIONS[compression][:flag]
29
- @compression_ext = COMPRESSIONS[compression][:ext]
30
- @time = Time.now
31
- @gpg_key = gpg_key
32
-
33
- Dir.mkdir(@backup_dir) unless File.directory?(@backup_dir)
34
- end
35
-
36
- def snar
37
- "backup-#{@name}.snar"
38
- end
39
-
40
- def snar_path
41
- File.join(@backup_dir, snar)
42
- end
43
-
44
- def snar_exists?
45
- File.exists?(snar_path)
46
- end
47
-
48
- def archive
49
- return @archive if @archive
50
- type = snar_exists? ? 'incr' : 'full'
51
- encrypted_bit = @gpg_key ? ".#{ENCRYPTED_EXTENSION}" : ''
52
- File.join(@backup_dir, "backup-#{@name}-#{@time.strftime('%Y%m%d_%H%M%S')}-#{type}.#{@compression_ext}#{encrypted_bit}")
53
- end
54
-
55
- def backup_cmd(verbose=false)
56
- exclude = @exclude.map{ |e| " --exclude \"#{e}\""}.join
57
- sources = @sources.map{ |s| "\"#{s}\""}.join(' ')
58
- @archive = archive
59
- tar_archive = @gpg_key ? '' : "f \"#{@archive}\""
60
- gpg_cmd = @gpg_key ? " | gpg -r #{@gpg_key} -o \"#{@archive}\" --always-trust --yes --batch --no-tty -e" : ''
61
- "tar c#{verbose ? 'v' : ''}#{tar_archive} #{@compression_flag} -g \"#{snar_path}\"#{exclude} --no-check-device #{sources}#{gpg_cmd}"
62
- end
63
-
64
- def self.parse_object(object, profile)
65
- name = File.basename(object.key)
66
- match = name.match(/^backup-([\w\-]+)-(\d\d\d\d)(\d\d)(\d\d)_(\d\d)(\d\d)(\d\d)-(\w+)\.(.*?)(\.#{ENCRYPTED_EXTENSION})?$/)
67
- return nil unless match && match[1] == profile
68
-
69
- return {
70
- :type => match[8].to_sym,
71
- :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),
72
- :name => name,
73
- :ext => match[9],
74
- :size => object.content_length,
75
- :profile => match[1],
76
- :compression => COMPRESSIONS.find{ |k,v| v[:ext] == match[9] }[0],
77
- :encryption => !match[10].nil?
78
- }
79
- end
80
-
81
- # No real point in creating a whole new class for this one
82
- def self.restore_cmd(restore_into, restore_from, verbose=false)
83
- ext, encrypted = restore_from.match(/[^\.\\\/]+\.(.*?)(\.#{ENCRYPTED_EXTENSION})?$/)[1..2]
84
- compression_flag = COMPRESSIONS.find{ |k,v| v[:ext] == ext }[1][:flag]
85
- tar_archive = encrypted ? '' : "f \"#{restore_from}\""
86
- gpg_cmd = encrypted ? "gpg --yes --batch --no-tty -d \"#{restore_from}\" | " : ''
87
- "#{gpg_cmd}tar xp#{verbose ? 'v' : ''}#{tar_archive} #{compression_flag} -G -C #{restore_into}"
88
- end
89
-
90
- end
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
- class IniParser
3
- @config
4
- @comments
5
- @file_path
6
- @defaults
7
-
8
- def initialize(file_path, defaults={})
9
- @file_path, @defaults = file_path, defaults
10
- end
11
-
12
- # Loads the config from file, and parses it
13
- def load
14
- if File.exists?(@file_path) && !File.directory?(@file_path)
15
- File.open(@file_path) do |f|
16
- @config, @comments = parse_config(f.readlines)
17
- end
18
- else
19
- @config, @comments = {}, {}
20
- end
21
- apply_defaults(@defaults)
22
- self # Allow chaining
23
- end
24
-
25
- # Saves the config to file
26
- def save
27
- File.open(@file_path, 'w') do |f|
28
- f.write(render_config)
29
- end
30
- end
31
-
32
- # Parses a set of lines in a config file, turning them into sections and comments
33
- def parse_config(config_lines)
34
- # TODO get rid of all the {:before => [], :after => nil}
35
- config, comments = {}, {}
36
- section = nil
37
- next_comment = {:before => [], :after => nil}
38
-
39
- config_lines.each do |line|
40
- case line.chomp
41
- # Section
42
- when /^\[([\w\-]+)(?: "([\w\-]+)")?\]$/
43
- section = $1.chomp
44
- section << ".#{$2.chomp}" if $2
45
- section = section.to_sym
46
- config[section] = {} unless config.has_key?(section)
47
- comments[section] = {} unless comments.has_key?(section)
48
- next_comment = {:before => [], :after => nil}
49
- # key line
50
- when /^([\w\-]+)\s*=\s*([^;]*?)\s*(?:;\s+(.*))?$/
51
- raise "Config key before section" unless section
52
- key = $1.chomp.to_sym
53
- if config[section].has_key?(key)
54
- config[section][key] = [config[section][key]] unless config[section][key].is_a?(Array)
55
- config[section][key] << $2.chomp
56
- else
57
- config[section][key] = $2.chomp
58
- end
59
- # If we found a comment at the end of the line
60
- next_comment[:after] = $3 if $3
61
- comments[section][key] = next_comment unless next_comment == {:before => [], :after => nil}
62
- next_comment = {:before => [], :after => nil}
63
- when /;\s?(.*)/
64
- next_comment[:before] << $1
65
- end
66
- end
67
-
68
- [config, comments]
69
- end
70
-
71
- # Applies the defaults passed to the constructor
72
- def apply_defaults(defaults)
73
- defaults.each do |key, default|
74
- section, key = key.match(/(.*)\.(.*)/)[1..2]
75
-
76
- if default.is_a?(Array)
77
- default_val, comment = default
78
- else
79
- default_val, comment = default, nil
80
- end
81
-
82
- @config[section] = {} unless @config.has_key?(section)
83
- set("#{section}.#{key}", default_val, comment)
84
- end
85
- end
86
-
87
- # Takes the current config, and renders it
88
- def render_config(comments=true)
89
- r = ''
90
- @config.each do |section_key, section|
91
- section_key_parts = section_key.to_s.split('.')
92
- if section_key_parts.count > 1
93
- r << "\n[#{section_key_parts.shift} \"#{section_key_parts.join(' ')}\"]\n\n"
94
- else
95
- r << "\n[#{section_key}]\n\n"
96
- end
97
- section.each do |key, values|
98
- values = [*values]
99
- comments_before, comments_after = '', ''
100
- if comments && @comments.include?(section_key) && @comments[section_key].include?(key)
101
- comments_before = @comments[section_key][key][:before].inject(''){ |s,v| s << "; #{v}\n" }
102
- comments_after = " ; #{@comments[section_key][key][:after]}" if @comments[section_key][key][:after]
103
- end
104
- r << comments_before
105
- r << values.map{ |value| "#{key} = #{value}" }.join("\n")
106
- r << comments_after << "\n\n"
107
- end
108
- end
109
- r.lstrip.rstrip
110
- end
111
-
112
- def [](arg)
113
- get(arg)
114
- end
115
-
116
- # Used to retrieve a config value, with an optional default.
117
- # arg: The config key to get, in the form <section>.<key>
118
- # default: The value to return if the key doesn't exist.
119
- # This function will use type information from self.defaults / default, if available.
120
- # Example: config_object.get('section.key', 'default_value')
121
- def get(arg, default=nil)
122
- section, key = arg.match(/(.*)\.(.*)/)[1..2]
123
- section = section.to_sym
124
- key = key.to_sym
125
-
126
- unless @config.has_key?(section) && @config[section].has_key?(key)
127
- raise "Tried to access config key #{section}.#{key} which doesn't exist" if default.nil?
128
- return default
129
- end
130
-
131
- val = @config[section][key]
132
- # Is it one of the reserved keywords...?
133
- case val
134
- when 'True' then return true
135
- when 'False' then return false
136
- when 'None' then return nil
137
- end
138
-
139
- # Attempt to case... Is there a default?
140
- if default
141
- type = default.class
142
- elsif @defaults.has_key?("#{section}.#{key}")
143
- type = @defaults["#{section}.#{key}"].class
144
- # If default is of the form (value, comment)
145
- type = @defaults["#{section}.#{key}"][0].class if type.is_a?(Array)
146
- else
147
- type = nil
148
- end
149
-
150
- case type
151
- when Integer
152
- return val.to_i
153
- when Float
154
- return val.to_f
155
- else
156
- return val
157
- end
158
- end
159
-
160
- def []=(arg, value)
161
- set(arg, value)
162
- end
163
-
164
- # Used to set a config value, with optional comments.
165
- # arg; The config key to set, in the form <section>.<key>
166
- # comments: The comments to set, if any. If multiple lines are desired, they should be separated by "\n"
167
- # Example: config_object.set('section.key', 'value', 'This is the comment\nExplaining section.key')
168
- def set(arg, value, comments=nil)
169
- section, key = arg.match(/(.*)\.(.*)/)[1..2]
170
- section = section.to_sym
171
- key = key.to_sym
172
-
173
- # Is it one of our special values?
174
- case value
175
- when true then value = 'True'
176
- when false then value = 'False'
177
- when nil then value = 'None'
178
- end
179
-
180
- @config[section] = {} unless @config.has_key?(section)
181
- @config[section][key] = value
182
-
183
- if comments
184
- comments = comments.split("\n")
185
- @comments[section] = {} unless @comments.has_key?(section)
186
- @comments[section][key] = {:before => comments, :after => nil}
187
- end
188
- end
189
-
190
- def has_section?(section)
191
- @config.has_key?(section.to_sym)
192
- end
193
-
194
- def find_sections(pattern=/.*/)
195
- @config.select{ |k,v| k =~ pattern }
196
- end
197
-
198
- def each
199
- @config.each_with_index do |section_key, section|
200
- section.each_with_index do |key, value|
201
- key_str = "#{section_key}#{key}"
202
- yield key_str, get(key_str)
203
- end
204
- end
205
- end
206
- end
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