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.
@@ -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