outback 0.0.14 → 1.0.2
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/CHANGELOG +6 -0
- data/LICENSE +21 -0
- data/README.md +29 -3
- data/lib/outback.rb +16 -11
- data/lib/outback/archive.rb +6 -17
- data/lib/outback/backup.rb +24 -19
- data/lib/outback/cli.rb +6 -2
- data/lib/outback/configuration.rb +15 -10
- data/lib/outback/directory_source.rb +8 -7
- data/lib/outback/directory_target.rb +18 -11
- data/lib/outback/encryption_processor.rb +34 -0
- data/lib/outback/errors.rb +7 -0
- data/lib/outback/logging.rb +7 -0
- data/lib/outback/mysql_source.rb +9 -9
- data/lib/outback/processor.rb +17 -0
- data/lib/outback/s3_target.rb +18 -9
- data/lib/outback/sftp_target.rb +70 -0
- data/lib/outback/source.rb +9 -2
- data/lib/outback/source_archive.rb +17 -0
- data/lib/outback/support/attr_setter.rb +1 -1
- data/lib/outback/support/configurable.rb +5 -3
- data/lib/outback/target.rb +51 -14
- data/lib/outback/target_archive.rb +30 -0
- data/lib/outback/version.rb +3 -0
- data/lib/vendor/enumerable_ext.rb +9 -0
- data/lib/{outback/vendor → vendor}/metaclass.rb +1 -1
- data/lib/{outback/vendor → vendor}/methodphitamine.rb +1 -1
- data/lib/vendor/mysql.rb +1093 -0
- data/lib/vendor/mysql/charset.rb +325 -0
- data/lib/vendor/mysql/constants.rb +165 -0
- data/lib/vendor/mysql/error.rb +989 -0
- data/lib/vendor/mysql/packet.rb +78 -0
- data/lib/vendor/mysql/protocol.rb +770 -0
- data/lib/vendor/numeric_ext.rb +49 -0
- data/lib/vendor/string_ext.rb +19 -0
- metadata +53 -39
- data/MIT-LICENSE +0 -20
- data/VERSION +0 -1
- data/lib/outback/configuration_error.rb +0 -4
- data/lib/outback/directory_archive.rb +0 -8
- data/lib/outback/local_archive.rb +0 -6
- data/lib/outback/s3_archive.rb +0 -18
- data/lib/outback/temp_archive.rb +0 -5
- data/lib/outback/vendor/mysql.rb +0 -1214
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f2a87a0171b3d8506d69d5d8b0611916a7bccd8a
|
4
|
+
data.tar.gz: d86ec7bddb738e5b9574073e06aae4ffa65b25c8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f252a3dab0597a9578c62c94ae4a9b739068c7090d357861ec534db8ae83f2327b6b5eead6cdc7e45e14f3fd502397cb6747d1ac7f900aba4692087d8004921d
|
7
|
+
data.tar.gz: 3e5a5ef024af75c835dbb4fae15f6b9e0d193c2e4865223beac15a33f83bef2b1c5345ff8a2795fc6b81634594d447ec294aedb04ae7a2b05dd2dd835140dd47
|
data/CHANGELOG
CHANGED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 onrooby
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
@@ -28,7 +28,7 @@ instantiated during a single run.
|
|
28
28
|
|
29
29
|
```` ruby
|
30
30
|
Outback::Configuration.new 'name' do
|
31
|
-
source :directory, '/
|
31
|
+
source :directory, '/var/www' do
|
32
32
|
exclude '/var/www/foo'
|
33
33
|
exclude '/var/www/icons/*.png'
|
34
34
|
end
|
@@ -45,6 +45,13 @@ Outback::Configuration.new 'name' do
|
|
45
45
|
# database 'specific_database'
|
46
46
|
end
|
47
47
|
|
48
|
+
processor :encryption do
|
49
|
+
password 'very secure encryption password'
|
50
|
+
|
51
|
+
# The default cipher is aes-256-cbc
|
52
|
+
#cipher 'openssl supported cipher'
|
53
|
+
end
|
54
|
+
|
48
55
|
# Amazon S3 storage
|
49
56
|
target :s3 do
|
50
57
|
access_key 'S3 access key'
|
@@ -56,6 +63,19 @@ Outback::Configuration.new 'name' do
|
|
56
63
|
# Just omit the definition to keep archives forever.
|
57
64
|
ttl 1.month
|
58
65
|
end
|
66
|
+
|
67
|
+
# SFTP storage
|
68
|
+
target :sftp, 'example.com' do
|
69
|
+
user 'backupuser'
|
70
|
+
# Usually, SSH keys will be used to connect to the server.
|
71
|
+
# Alternatively, a password can be given here.
|
72
|
+
#password 'your password'
|
73
|
+
|
74
|
+
# Default port is 22
|
75
|
+
# port 1234
|
76
|
+
ttl 1.day
|
77
|
+
path '/backups'
|
78
|
+
end
|
59
79
|
|
60
80
|
# Store on a local filesystem path
|
61
81
|
target :directory, '/media/backups/daily' do
|
@@ -79,7 +99,7 @@ Default configurations and commandline options
|
|
79
99
|
If you place your backup configurations in the file `/etc/outback.conf` they
|
80
100
|
will be read automatically when the outback executable is invoked. Make
|
81
101
|
sure to have correct permissions on the configuration files, as they might
|
82
|
-
include database passwords.
|
102
|
+
include database and encryption passwords.
|
83
103
|
|
84
104
|
Alternatively, you can pass in the configuration file to read as a
|
85
105
|
commandline argument. The default configuration file in /etc will then be
|
@@ -131,4 +151,10 @@ Other commandline options are:
|
|
131
151
|
* `-t` `--test` test configuration, then exit
|
132
152
|
* `-l` `--list` list available configurations, then exit
|
133
153
|
* `-h` `--help` display help
|
134
|
-
* `--version` display version
|
154
|
+
* `--version` display version
|
155
|
+
|
156
|
+
Known Issues
|
157
|
+
------------
|
158
|
+
|
159
|
+
The s3 gem currently doesn't support V4 signatures, which are required for
|
160
|
+
s3 regions introduced after January 2014.
|
data/lib/outback.rb
CHANGED
@@ -3,35 +3,40 @@ require 'fileutils'
|
|
3
3
|
require 'tempfile'
|
4
4
|
require 'tmpdir'
|
5
5
|
|
6
|
-
require 'active_support/core_ext'
|
7
|
-
|
8
6
|
require 's3'
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
require_relative 'vendor/string_ext'
|
9
|
+
require_relative 'vendor/numeric_ext'
|
10
|
+
require_relative 'vendor/enumerable_ext'
|
11
|
+
require_relative 'vendor/mysql'
|
12
|
+
require_relative 'vendor/metaclass'
|
13
|
+
require_relative 'vendor/methodphitamine'
|
13
14
|
|
15
|
+
require 'outback/version'
|
14
16
|
require 'outback/support/returning'
|
15
17
|
require 'outback/support/attr_setter'
|
16
18
|
require 'outback/support/configurable'
|
17
19
|
require 'outback/support/mysql_ext'
|
18
20
|
require 'outback/support/pathname_ext'
|
21
|
+
require 'outback/errors'
|
22
|
+
require 'outback/logging'
|
19
23
|
require 'outback/configuration'
|
20
|
-
require 'outback/
|
24
|
+
require 'outback/archive'
|
21
25
|
require 'outback/source'
|
26
|
+
require 'outback/source_archive'
|
22
27
|
require 'outback/directory_source'
|
23
28
|
require 'outback/mysql_source'
|
24
|
-
require 'outback/
|
25
|
-
require 'outback/
|
29
|
+
require 'outback/processor'
|
30
|
+
require 'outback/encryption_processor'
|
26
31
|
require 'outback/target'
|
32
|
+
require 'outback/target_archive'
|
27
33
|
require 'outback/directory_target'
|
28
|
-
require 'outback/directory_archive'
|
29
34
|
require 'outback/s3_target'
|
30
|
-
require 'outback/
|
35
|
+
require 'outback/sftp_target'
|
31
36
|
require 'outback/backup'
|
32
37
|
|
33
38
|
module Outback
|
34
|
-
|
39
|
+
TIME_FORMAT = '%Y%m%d%H%M%S'.freeze
|
35
40
|
|
36
41
|
class << self
|
37
42
|
%w(verbose silent).each do |method|
|
data/lib/outback/archive.rb
CHANGED
@@ -1,30 +1,19 @@
|
|
1
1
|
module Outback
|
2
2
|
class Archive
|
3
|
-
NAME_PATTERN =
|
3
|
+
NAME_PATTERN = /\A([A-Za-z0-9.\-]+)_(\d{14})_(\w+)/
|
4
4
|
|
5
|
-
attr_reader :filename, :backup_name, :timestamp, :source_name, :
|
5
|
+
attr_reader :filename, :backup_name, :timestamp, :source_name, :size
|
6
6
|
|
7
|
-
def initialize(filename
|
8
|
-
@filename
|
7
|
+
def initialize(filename)
|
8
|
+
@filename = Pathname.new(filename)
|
9
9
|
unless match_data = @filename.basename.to_s.match(NAME_PATTERN)
|
10
10
|
raise ArgumentError, 'invalid name'
|
11
11
|
end
|
12
12
|
@backup_name, @timestamp, @source_name = match_data.captures[0..2]
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
filename
|
15
|
+
def to_s
|
16
|
+
"#{filename}"
|
17
17
|
end
|
18
|
-
|
19
|
-
def open
|
20
|
-
filename.open
|
21
|
-
end
|
22
|
-
|
23
|
-
def outdated?
|
24
|
-
if timestamp && parent && parent.ttl
|
25
|
-
Time.now - Time.parse(timestamp) > parent.ttl
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
18
|
end
|
30
19
|
end
|
data/lib/outback/backup.rb
CHANGED
@@ -1,44 +1,49 @@
|
|
1
1
|
module Outback
|
2
2
|
class Backup
|
3
|
-
|
4
|
-
delegate :sources, :targets, :to => :configuration
|
3
|
+
include Logging
|
5
4
|
|
5
|
+
attr_reader :configuration, :name, :timestamp, :tmpdir
|
6
|
+
|
6
7
|
def initialize(configuration)
|
7
8
|
raise ArgumentError, "configuration required" unless configuration.is_a?(Outback::Configuration)
|
8
9
|
@configuration = configuration
|
9
10
|
@name = configuration.name
|
10
|
-
@timestamp = Time.now.
|
11
|
+
@timestamp = Time.now.strftime(Outback::TIME_FORMAT)
|
11
12
|
end
|
12
13
|
|
13
14
|
def run!
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
ensure
|
22
|
-
FileUtils.remove_entry_secure(tmpdir)
|
23
|
-
end
|
15
|
+
logger.info "Using working directory #{configuration.tmpdir}" if configuration.tmpdir
|
16
|
+
@tmpdir = Dir.mktmpdir([name, timestamp], configuration.tmpdir)
|
17
|
+
archives = create_archives
|
18
|
+
logger.info "Created #{archives.size} archives"
|
19
|
+
archives = process_archives(archives)
|
20
|
+
logger.info "Processed #{archives.size} archives"
|
21
|
+
store_archives(archives)
|
24
22
|
purge_targets
|
23
|
+
ensure
|
24
|
+
FileUtils.remove_entry_secure(tmpdir)
|
25
|
+
end
|
26
|
+
|
27
|
+
[:sources, :processors, :targets].each do |collection|
|
28
|
+
define_method(collection) { configuration.public_send(collection) }
|
25
29
|
end
|
26
30
|
|
27
31
|
private
|
28
32
|
|
29
33
|
def create_archives
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
sources.map { |source| source.create_archives(timestamp, tmpdir) }.flatten.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_archives(archives)
|
38
|
+
processors.inject(archives) { |archives, processor| processor.process!(archives) }.flatten.compact
|
34
39
|
end
|
35
40
|
|
36
|
-
def store_archives
|
41
|
+
def store_archives(archives)
|
37
42
|
targets.each { |target| target.put(archives) }
|
38
43
|
end
|
39
44
|
|
40
45
|
def purge_targets
|
41
|
-
targets.each { |target| target.purge!
|
46
|
+
targets.each { |target| target.purge! }
|
42
47
|
end
|
43
48
|
|
44
49
|
end
|
data/lib/outback/cli.rb
CHANGED
@@ -35,7 +35,11 @@ module Outback
|
|
35
35
|
end
|
36
36
|
option_parser.parse!
|
37
37
|
Outback::Configuration.reset
|
38
|
-
config_file = ARGV.first
|
38
|
+
config_file = if ARGV.first
|
39
|
+
ARGV.first.start_with?('/') ? ARGV.first : File.join(Dir.pwd, ARGV.first)
|
40
|
+
else
|
41
|
+
DEFAULT_CONFIGURATION_FILE
|
42
|
+
end
|
39
43
|
begin
|
40
44
|
load config_file
|
41
45
|
rescue ConfigurationError => conf_error
|
@@ -83,4 +87,4 @@ module Outback
|
|
83
87
|
end
|
84
88
|
|
85
89
|
end
|
86
|
-
end
|
90
|
+
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
module Outback
|
2
2
|
class Configuration
|
3
|
+
|
3
4
|
@loaded = []
|
4
5
|
|
5
6
|
class << self
|
6
7
|
def add(configuration)
|
7
|
-
raise ConfigurationError
|
8
|
+
raise ConfigurationError, "duplicate configuration #{configuration.name}" if loaded.any?(&its.name == configuration.name)
|
8
9
|
loaded << configuration
|
9
10
|
end
|
10
11
|
|
@@ -21,13 +22,13 @@ module Outback
|
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
|
-
attr_reader :name, :sources, :targets, :errors
|
25
|
+
attr_reader :name, :sources, :targets, :processors, :errors
|
25
26
|
|
26
27
|
def initialize(name, &block)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@sources, @targets, @errors = [], [], []
|
28
|
+
name = name.to_s
|
29
|
+
raise ConfigurationError, "Illegal configuration name #{name.inspect}" unless name.match(/\A[a-z][a-z0-9_\-.]+\z/)
|
30
|
+
@name = name
|
31
|
+
@sources, @processors, @targets, @errors = [], [], [], []
|
31
32
|
if block_given?
|
32
33
|
if block.arity == 1 then yield(self) else instance_eval(&block) end
|
33
34
|
end
|
@@ -48,21 +49,25 @@ module Outback
|
|
48
49
|
@tmpdir
|
49
50
|
elsif args.size == 1
|
50
51
|
dir = Pathname.new(args.first).realpath
|
51
|
-
raise
|
52
|
+
raise ConfigurationError, "tmpdir #{dir} is not a directory" unless dir.directory?
|
52
53
|
@tmpdir = dir
|
53
54
|
else
|
54
|
-
raise ConfigurationError
|
55
|
+
raise ConfigurationError, "tmpdir: wrong number of arguments(#{args.size} for 1)"
|
55
56
|
end
|
56
57
|
end
|
57
58
|
|
58
59
|
protected
|
59
60
|
|
60
61
|
def source(type, *args, &block)
|
61
|
-
"Outback::#{type.to_s.classify}Source".constantize.configure(*args, &block).tap { |instance| sources << instance }
|
62
|
+
"Outback::#{type.to_s.classify}Source".constantize.configure(name, *args, &block).tap { |instance| sources << instance }
|
63
|
+
end
|
64
|
+
|
65
|
+
def processor(type, *args, &block)
|
66
|
+
"Outback::#{type.to_s.classify}Processor".constantize.configure(name, *args, &block).tap { |instance| processors << instance }
|
62
67
|
end
|
63
68
|
|
64
69
|
def target(type, *args, &block)
|
65
|
-
"Outback::#{type.to_s.classify}Target".constantize.configure(*args, &block).tap { |instance| targets << instance }
|
70
|
+
"Outback::#{type.to_s.classify}Target".constantize.configure(name, *args, &block).tap { |instance| targets << instance }
|
66
71
|
end
|
67
72
|
|
68
73
|
def error(message)
|
@@ -2,7 +2,8 @@ module Outback
|
|
2
2
|
class DirectorySource < Source
|
3
3
|
attr_reader :path
|
4
4
|
|
5
|
-
def initialize(path)
|
5
|
+
def initialize(backup_name, path)
|
6
|
+
super(backup_name)
|
6
7
|
@path = path
|
7
8
|
end
|
8
9
|
|
@@ -18,18 +19,18 @@ module Outback
|
|
18
19
|
excludes.concat(paths.map(&:to_s)).uniq!
|
19
20
|
end
|
20
21
|
|
21
|
-
def create_archives(
|
22
|
+
def create_archives(timestamp, tmpdir)
|
22
23
|
source_dir = Pathname.new(path).realpath
|
23
24
|
archive_name = Pathname.new(tmpdir).join("#{backup_name}_#{timestamp}_#{source_name}.tar.gz")
|
24
25
|
exclude_list = Pathname.new(tmpdir).join('exclude_list.txt')
|
25
26
|
File.open(exclude_list, 'w') { |f| f << excludes.map { |e| e.to_s.sub(/\A\//, '') }.join("\n") }
|
26
27
|
verbose_switch = Outback.verbose? ? 'v' : ''
|
27
28
|
commandline = "tar -cz#{verbose_switch}pf #{archive_name} --exclude-from=#{exclude_list} --directory=/ #{source_dir.to_s.sub(/\A\//, '')}"
|
28
|
-
|
29
|
+
logger.debug "executing command: #{commandline}"
|
29
30
|
result = `#{commandline}`
|
30
|
-
|
31
|
-
|
32
|
-
[
|
31
|
+
logger.debug "result: #{result}"
|
32
|
+
logger.info "Archived directory #{path}"
|
33
|
+
[SourceArchive.new(archive_name)]
|
33
34
|
end
|
34
35
|
end
|
35
|
-
end
|
36
|
+
end
|
@@ -3,7 +3,8 @@ module Outback
|
|
3
3
|
attr_reader :path
|
4
4
|
attr_setter :user, :group, :directory_permissions, :archive_permissions, :ttl, :move
|
5
5
|
|
6
|
-
def initialize(path)
|
6
|
+
def initialize(backup_name, path)
|
7
|
+
super(backup_name)
|
7
8
|
@path = Pathname.new(path)
|
8
9
|
end
|
9
10
|
|
@@ -11,39 +12,45 @@ module Outback
|
|
11
12
|
(user and group) or (not user and not group)
|
12
13
|
end
|
13
14
|
|
14
|
-
def
|
15
|
+
def to_s
|
15
16
|
"directory:#{path}"
|
16
17
|
end
|
17
18
|
|
18
19
|
def put(archives)
|
19
|
-
|
20
|
+
FileUtils.mkdir_p(path) unless path.directory?
|
20
21
|
FileUtils.chmod directory_permissions || 0700, path
|
21
22
|
size = 0
|
22
23
|
archives.each do |archive|
|
23
24
|
basename = Pathname.new(archive.filename).basename
|
24
25
|
if move
|
25
|
-
|
26
|
+
logger.debug "moving #{archive.filename} to #{path}"
|
26
27
|
FileUtils.mv archive.filename, path
|
27
28
|
else
|
28
|
-
|
29
|
+
logger.debug "copying #{archive.filename} to #{path}"
|
29
30
|
FileUtils.cp_r archive.filename, path
|
30
31
|
end
|
31
32
|
archived_file = path.join(basename)
|
32
|
-
|
33
|
+
logger.debug "setting permissions for #{archived_file}"
|
33
34
|
FileUtils.chmod archive_permissions || 0600, archived_file
|
34
35
|
if user && group
|
35
|
-
|
36
|
+
logger.debug "setting owner #{user}, group #{group} for #{archived_file}"
|
36
37
|
FileUtils.chown user, group, archived_file
|
37
38
|
end
|
38
39
|
size += archived_file.size
|
39
40
|
end
|
40
|
-
|
41
|
+
logger.info "#{move ? 'Moved' : 'Copied'} #{archives.size} archives (#{size} bytes) to #{self}"
|
41
42
|
archives.size
|
42
43
|
end
|
44
|
+
|
45
|
+
private
|
43
46
|
|
44
|
-
def
|
45
|
-
path.files(Archive::NAME_PATTERN).map { |f|
|
47
|
+
def list_all_archives
|
48
|
+
path.files(Archive::NAME_PATTERN).map { |f| build_archive(f.to_s, f.size) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def unlink_archive!(archive)
|
52
|
+
archive.filename.unlink
|
46
53
|
end
|
47
54
|
|
48
55
|
end
|
49
|
-
end
|
56
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
module Outback
|
5
|
+
class EncryptionProcessor < Processor
|
6
|
+
|
7
|
+
attr_setter :password, :cipher
|
8
|
+
|
9
|
+
def cipher
|
10
|
+
@cipher ||= 'aes-256-cbc'
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"encryption:#{cipher}"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def process_archive!(archive)
|
20
|
+
result = nil
|
21
|
+
outfile = Pathname.new("#{archive.filename}.enc")
|
22
|
+
logger.debug "Encrypting #{archive} with #{self}"
|
23
|
+
Open3.popen3("openssl enc -#{cipher} -pass stdin -in #{archive.filename} -out #{outfile}") do |stdin, stdout, stderr, wait_thr|
|
24
|
+
stdin << password
|
25
|
+
stdin.close
|
26
|
+
result = wait_thr.value
|
27
|
+
end
|
28
|
+
raise ProcessingError, "error processing archive #{archive} in #{self}" unless result.success?
|
29
|
+
raise ProcessingError, "outfile #{outfile} not found" unless outfile.file?
|
30
|
+
archive.unlink
|
31
|
+
SourceArchive.new(outfile)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|