simple_backup 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +2 -1
  4. data/backup_example.rb +23 -13
  5. data/lib/simple_backup/backend/abstract.rb +35 -0
  6. data/lib/simple_backup/backend/local.rb +45 -0
  7. data/lib/simple_backup/backends.rb +60 -0
  8. data/lib/simple_backup/dsl.rb +26 -93
  9. data/lib/simple_backup/engine.rb +41 -3
  10. data/lib/simple_backup/source/abstract.rb +130 -0
  11. data/lib/simple_backup/source/dir.rb +39 -0
  12. data/lib/simple_backup/source/dir_strategy/bare.rb +17 -0
  13. data/lib/simple_backup/source/dir_strategy/capistrano.rb +41 -0
  14. data/lib/simple_backup/source/file.rb +20 -0
  15. data/lib/simple_backup/source/mysql.rb +28 -0
  16. data/lib/simple_backup/sources.rb +74 -0
  17. data/lib/simple_backup/utils/disk_usage.rb +53 -0
  18. data/lib/simple_backup/utils/logger.rb +92 -0
  19. data/lib/simple_backup/utils/mailer.rb +126 -0
  20. data/lib/simple_backup/utils/mysql.rb +65 -0
  21. data/lib/simple_backup/utils.rb +4 -53
  22. data/lib/simple_backup/version.rb +2 -2
  23. data/lib/simple_backup.rb +40 -5
  24. metadata +17 -19
  25. data/lib/simple_backup/engine/abstract.rb +0 -42
  26. data/lib/simple_backup/engine/app_strategy/abstract.rb +0 -13
  27. data/lib/simple_backup/engine/app_strategy/bare.rb +0 -29
  28. data/lib/simple_backup/engine/app_strategy/capistrano.rb +0 -44
  29. data/lib/simple_backup/engine/app_strategy/factory.rb +0 -20
  30. data/lib/simple_backup/engine/apps.rb +0 -63
  31. data/lib/simple_backup/engine/mysql.rb +0 -104
  32. data/lib/simple_backup/exception/app_already_defined.rb +0 -6
  33. data/lib/simple_backup/exception/apps_dir_does_not_exists.rb +0 -6
  34. data/lib/simple_backup/exception/base.rb +0 -6
  35. data/lib/simple_backup/exception/cant_create_dir.rb +0 -6
  36. data/lib/simple_backup/exception/type_does_not_exists.rb +0 -6
  37. data/lib/simple_backup/exception.rb +0 -5
  38. data/lib/simple_backup/logger.rb +0 -84
  39. data/lib/simple_backup/mailer.rb +0 -138
  40. data/lib/simple_backup/storage.rb +0 -96
@@ -0,0 +1,28 @@
1
+ module SimpleBackup
2
+ module Source
3
+ class Mysql < Abstract
4
+ @@mysql = Utils::MySQL.instance
5
+
6
+ def configure(db, options = {})
7
+ @db = db
8
+
9
+ @exclude_tables = options[:exclude_tables] if options[:exclude_tables]
10
+ end
11
+
12
+ private
13
+ def prepare_data
14
+ @@mysql.open
15
+
16
+ tables = @@mysql.scan_tables(@db)
17
+ return false if tables.nil?
18
+
19
+ tables = tables - @exclude_tables if @exclude_tables
20
+ dumpfile = ::File.join(@tmp_dir, @db) + '.sql'
21
+
22
+ @@mysql.dump(@db, tables, dumpfile)
23
+
24
+ true
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ require 'singleton'
2
+ require 'simple_backup/source/abstract'
3
+
4
+ module SimpleBackup
5
+ class Sources
6
+ include Singleton
7
+
8
+ @@logger = Utils::Logger.instance
9
+
10
+ def initialize
11
+ @sources = {}
12
+ @default_keep_last = 5
13
+ end
14
+
15
+ def default_keep_last=(value)
16
+ @default_keep_last = value
17
+ end
18
+
19
+ def each(&block)
20
+ @sources.each do |type, sources|
21
+ sources.each(&block)
22
+ end
23
+ end
24
+
25
+ def backup
26
+ @sources.each do |type, sources|
27
+ sources.each do |name, source|
28
+ source.get
29
+ end
30
+ end
31
+ end
32
+
33
+ def cleanup
34
+ each do |name, source|
35
+ source.cleanup
36
+ end
37
+ end
38
+
39
+ def method_missing(method, *args)
40
+ source = create_source(method)
41
+
42
+ return nil if source.nil?
43
+
44
+ name = args.shift
45
+ identifier = args.shift
46
+ options = args.shift
47
+ options ||= {}
48
+
49
+ type = source.type.to_sym
50
+ @sources[type] = {} if @sources[type].nil?
51
+ raise "Name '#{name}' for source #{type} already used" if @sources[type].has_key?(name.to_sym)
52
+
53
+ source.keep_last = @default_keep_last
54
+ source.keep_last = options[:keep_last] if options[:keep_last]
55
+ source.backends = options[:backends] if options[:backends]
56
+ source.name = name
57
+
58
+ source.configure(identifier, options)
59
+
60
+ @@logger.info "Created source for: #{source.desc.strip}"
61
+
62
+ @sources[type][name.to_sym] = source
63
+ end
64
+
65
+ private
66
+ def create_source(name)
67
+ file = "simple_backup/source/#{name}"
68
+
69
+ require file
70
+ source_name = Object.const_get("SimpleBackup::Source::#{name.capitalize}")
71
+ source_name.new
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,53 @@
1
+ module SimpleBackup
2
+ module Utils
3
+ class Disk
4
+ @@high_usage_treshold = 0.9
5
+ @@paths = []
6
+
7
+ def self.high_usage_treshold=(value)
8
+ @@high_usage_treshold = value.to_f
9
+
10
+ raise ArgumentError.new "Backuper::Utils::Disk::high_usage_treshold must be a float greater than zero" if @@high_usage_treshold <= 0.0
11
+ end
12
+
13
+ def self.high_usage_treshold
14
+ @@high_usage_treshold
15
+ end
16
+
17
+ def self.add_path(path)
18
+ @@paths << path unless @@paths.include?(path)
19
+ end
20
+
21
+ def self.usage
22
+ df = `df -m #{@@paths.join(' ')} 2>/dev/null`.split("\n")
23
+ df.shift
24
+
25
+ max_percent = 0.0;
26
+ df.map! do |row|
27
+ row = row.split(' ')
28
+
29
+ percent_usage = (row[4].gsub('%', '').to_f / 100).round(2)
30
+ row = {
31
+ mount: row[5],
32
+ fs: row[0],
33
+ size: row[1],
34
+ used: row[2],
35
+ available: row[3],
36
+ percent: percent_usage,
37
+ high_usage_exceeded: percent_usage >= @@high_usage_treshold
38
+ }
39
+
40
+ max_percent = row[:percent] if row[:percent] > max_percent
41
+
42
+ row
43
+ end
44
+
45
+ {
46
+ mounts: df.uniq,
47
+ high_usage_exceeded: max_percent >= @@high_usage_treshold,
48
+ high_usage: max_percent
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,92 @@
1
+ require 'colorize'
2
+ require 'singleton'
3
+
4
+ module SimpleBackup
5
+ module Utils
6
+ class Logger
7
+ include Singleton
8
+
9
+ TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
10
+
11
+ def initialize
12
+ @buffer = []
13
+ @scope = 0
14
+ @level = :info
15
+ @levels = {
16
+ debug: {weight: 3, color: :light_cyan},
17
+ info: {weight: 2, color: :green},
18
+ warning: {weight: 1, color: :light_yellow},
19
+ error: {weight: 0, color: :red}
20
+ }
21
+
22
+ banner = "LOG STARTED #{Time.new.strftime('%Y-%m-%dT%H:%M:%S')}"
23
+ banner2 = "SimpleBackup v#{SimpleBackup::Version::get}"
24
+
25
+ banner_length = 0
26
+ banner_length = banner.length if banner.length > banner_length
27
+ banner_length = banner2.length if banner2.length > banner_length
28
+ banner_length = 80 if 80 > banner_length
29
+
30
+ border = '=' * ((banner_length - banner.length) / 2).ceil.to_i
31
+ @buffer << "#{border}==[ #{banner} ]==#{border}"
32
+ border = '=' * ((banner_length - banner2.length) / 2).ceil.to_i
33
+ @buffer << "#{border}==[ #{banner2} ]==#{border}"
34
+
35
+ puts @buffer[0].green
36
+ puts @buffer[1].green
37
+ end
38
+
39
+ def level=(level)
40
+ check_level(level)
41
+ @level = level
42
+ end
43
+
44
+ def scope_start(level = nil, message = nil)
45
+ log level, message unless level.nil? and message.nil?
46
+ @scope += 1
47
+ end
48
+
49
+ def scope_end(level = nil, message = nil)
50
+ log level, message unless level.nil? and message.nil?
51
+ @scope -= 1 unless @scope == 0
52
+ end
53
+
54
+ def debug(message)
55
+ log(:debug, message)
56
+ end
57
+
58
+ def info(message)
59
+ log(:info, message)
60
+ end
61
+
62
+ def warning(message)
63
+ log(:warning, message)
64
+ end
65
+
66
+ def error(message)
67
+ log(:error, message)
68
+ end
69
+
70
+ def log(level, message)
71
+ check_level(level)
72
+
73
+ color = @levels[level][:color]
74
+ should_write = @levels[level][:weight] <= @levels[@level][:weight]
75
+
76
+ scope_prefix = '..' * @scope
77
+ message = "%s %7s: %s%s" % [Time.new.strftime(TIME_FORMAT), level.to_s.upcase, scope_prefix, message]
78
+ @buffer << message
79
+
80
+ puts message.colorize(color: color) if should_write
81
+ end
82
+
83
+ def check_level(level)
84
+ raise "Unknown logging level #{level}" unless @levels.has_key?(level)
85
+ end
86
+
87
+ def buffer
88
+ @buffer
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,126 @@
1
+ require 'mail'
2
+ require 'socket'
3
+
4
+ module SimpleBackup
5
+ module Utils
6
+ class Mailer
7
+ @@logger = Logger.instance
8
+
9
+ def initialize()
10
+ @to = []
11
+ @cc = []
12
+ @bcc = []
13
+ @hostname = Socket.gethostbyname(Socket.gethostname).first
14
+ end
15
+
16
+ def subject_prefix(prefix)
17
+ @subject_prefix = prefix
18
+ end
19
+
20
+ def from(from)
21
+ @from = from
22
+ end
23
+
24
+ def to(to)
25
+ @to << to
26
+ end
27
+
28
+ def cc(cc)
29
+ @cc << cc
30
+ end
31
+
32
+ def bcc(bcc)
33
+ @bcc << bcc
34
+ end
35
+
36
+ def send
37
+ @@logger.info "Setting sender to: #{@from}"
38
+ from = @from
39
+ @@logger.scope_start :info, "Adding recipients:"
40
+ to = @to
41
+ to.each do |mail|
42
+ @@logger.info "to: #{mail}"
43
+ end
44
+ cc = @cc
45
+ cc.each do |mail|
46
+ @@logger.info "cc: #{mail}"
47
+ end
48
+ bcc = @bcc
49
+ bcc.each do |mail|
50
+ @@logger.info "bcc: #{mail}"
51
+ end
52
+ @@logger.scope_end
53
+
54
+ @subject_prefix += '[FAILED]' if SimpleBackup.status == :failed
55
+
56
+ subject = "%s Backup %s for %s" % [@subject_prefix, TIMESTAMP, @hostname]
57
+ @@logger.debug "Subject: #{subject}"
58
+
59
+ body = get_body
60
+
61
+ mail = Mail.new do
62
+ from from
63
+ to to
64
+ cc cc
65
+ bcc bcc
66
+ subject subject.strip
67
+ body body
68
+ end
69
+
70
+ mail.delivery_method :sendmail
71
+ @@logger.debug "Setting delivery method to sendmail"
72
+
73
+ mail.deliver
74
+ @@logger.info "Notification sent"
75
+ end
76
+
77
+ private
78
+ def get_body
79
+ sources = ''
80
+
81
+ SimpleBackup::Sources.instance.each do |name, source|
82
+ sources += " - %s\n" % source.desc
83
+ end
84
+
85
+ body = <<MAIL
86
+ Hi,
87
+
88
+ Backup #{TIMESTAMP} was created!
89
+
90
+ Backup contains:
91
+ #{sources}
92
+ Disk usage after backup:
93
+ #{disk_usage}
94
+ Backup log:
95
+ ------------
96
+ #{@@logger.buffer.join("\n")}
97
+ ------------
98
+
99
+ Have a nice day,
100
+ SimpleBackup
101
+
102
+ --
103
+ Mail was send automatically
104
+ Do not respond!
105
+ MAIL
106
+
107
+ body
108
+ end
109
+
110
+ def disk_usage
111
+ content = "%16s %25s %12s %12s %12s %12s\n" % ['Mount', 'Filesystem', 'Size', 'Used', 'Available', 'Percent used']
112
+
113
+ usage = Utils::Disk::usage
114
+ usage[:mounts].each do |m|
115
+ percent_usage = (m[:percent] * 100).to_s
116
+ percent_usage = '(!!) ' + percent_usage if m[:high_usage_exceeded]
117
+ content += "%16s %25s %8s MiB %8s MiB %8s MiB %11s%%\n" % [m[:mount], m[:fs], m[:size], m[:used], m[:available], percent_usage]
118
+ end
119
+
120
+ content += "\nHigh usage treshold exceeded!\nMax usage is #{usage[:high_usage]} where treshold is set to #{Utils::Disk::high_usage_treshold}\n" if usage[:high_usage_exceeded]
121
+
122
+ content
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,65 @@
1
+ require 'singleton'
2
+ require 'mysql2'
3
+
4
+ module SimpleBackup
5
+ module Utils
6
+ class MySQL
7
+ include Singleton
8
+
9
+ @@logger = Logger.instance
10
+
11
+ def initialize
12
+ @host = 'localhost'
13
+ @port = 3306
14
+ @user = nil
15
+ @pass = nil
16
+ end
17
+
18
+ def open
19
+ return nil unless @conn.nil?
20
+
21
+ @conn = Mysql2::Client.new(host: @host, port: @port, username: @user, password: @pass)
22
+ @existing_dbs = []
23
+ @conn.query("SHOW DATABASES").each do |row|
24
+ @existing_dbs << row['Database']
25
+ end
26
+ end
27
+
28
+ def close
29
+ @conn.close unless @conn.nil?
30
+ end
31
+
32
+ def scan_tables(db)
33
+ return nil unless @existing_dbs.include?(db)
34
+
35
+ tables = []
36
+ @conn.query("SHOW TABLES FROM `#{db}`").each do |row|
37
+ tables << row["Tables_in_#{db}"]
38
+ end
39
+ tables
40
+ end
41
+
42
+ def dump(db, tables, dumpfile)
43
+ cmd = "mysqldump --flush-logs --flush-privileges --order-by-primary --complete-insert -C -h #{@host} -u #{@user} -p#{@pass} #{db} #{tables.join(' ')} > #{dumpfile}"
44
+ @@logger.debug "Running command: #{cmd}"
45
+ `#{cmd}`
46
+ end
47
+
48
+ def host(value)
49
+ @host = value
50
+ end
51
+
52
+ def port(value)
53
+ @port = value
54
+ end
55
+
56
+ def user(value)
57
+ @user = value
58
+ end
59
+
60
+ def pass(value)
61
+ @pass = value
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,53 +1,4 @@
1
- module SimpleBackup
2
- module Utils
3
- class Disk
4
- @@high_usage_treshold = 0.9
5
- @@paths = []
6
-
7
- def self.high_usage_treshold=(value)
8
- @@high_usage_treshold = value.to_f
9
-
10
- raise ArgumentError.new "Backuper::Utils::Disk::high_usage_treshold must be a float greater than zero" if @@high_usage_treshold <= 0.0
11
- end
12
-
13
- def self.high_usage_treshold
14
- @@high_usage_treshold
15
- end
16
-
17
- def self.add_path(path)
18
- @@paths << path unless @@paths.include?(path)
19
- end
20
-
21
- def self.usage
22
- df = `df -m #{@@paths.join(' ')} 2>/dev/null`.split("\n")
23
- df.shift
24
-
25
- max_percent = 0.0;
26
- df.map! do |row|
27
- row = row.split(' ')
28
-
29
- percent_usage = (row[4].gsub('%', '').to_f / 100).round(2)
30
- row = {
31
- mount: row[5],
32
- fs: row[0],
33
- size: row[1],
34
- used: row[2],
35
- available: row[3],
36
- percent: percent_usage,
37
- high_usage_exceeded: percent_usage >= @@high_usage_treshold
38
- }
39
-
40
- max_percent = row[:percent] if row[:percent] > max_percent
41
-
42
- row
43
- end
44
-
45
- {
46
- mounts: df.uniq,
47
- high_usage_exceeded: max_percent >= @@high_usage_treshold,
48
- high_usage: max_percent
49
- }
50
- end
51
- end
52
- end
53
- end
1
+ require 'simple_backup/utils/disk_usage'
2
+ require 'simple_backup/utils/logger'
3
+ require 'simple_backup/utils/mailer'
4
+ require 'simple_backup/utils/mysql'
@@ -1,8 +1,8 @@
1
1
  module SimpleBackup
2
2
  class Version
3
3
  MAJOR = 0
4
- MINOR = 2
5
- PATCH = 1
4
+ MINOR = 3
5
+ PATCH = 0
6
6
  PRE_RELEASE = nil
7
7
 
8
8
  def self.get
data/lib/simple_backup.rb CHANGED
@@ -1,11 +1,46 @@
1
- require 'simple_backup/utils'
2
1
  require 'simple_backup/version'
3
- require 'simple_backup/logger'
2
+ require 'simple_backup/utils'
4
3
  require 'simple_backup/dsl'
5
- require 'simple_backup/storage'
4
+ require 'simple_backup/sources'
5
+ require 'simple_backup/backends'
6
6
  require 'simple_backup/engine'
7
- require 'simple_backup/mailer'
8
- require 'simple_backup/exception'
9
7
 
10
8
  module SimpleBackup
9
+ TIMESTAMP = Time.new.strftime('%Y%m%d%H%M%S')
10
+
11
+ @@status = :failed
12
+ @@logger = Utils::Logger.instance
13
+
14
+ def self.status
15
+ @@status
16
+ end
17
+
18
+ def self.run(&block)
19
+ @@logger.scope_start :info, "Backup #{TIMESTAMP} started"
20
+
21
+ engine = Engine::Engine.new
22
+ dsl = DSL.new(engine)
23
+
24
+ @@logger.scope_start :info, "Configuration"
25
+ dsl.instance_eval(&block)
26
+ @@logger.scope_end
27
+
28
+ engine.run
29
+ @@status = :succeed
30
+
31
+ @@logger.scope_end :info, "Backup #{TIMESTAMP} finished"
32
+ rescue StandardError => e
33
+ self.handle_exception(e)
34
+ ensure
35
+ engine.notify if engine
36
+ end
37
+
38
+ def self.handle_exception(e)
39
+ @@logger.error "#{e.class} => #{e.message}"
40
+ @@logger.error "Backup #{TIMESTAMP} failed"
41
+
42
+ STDERR.puts "Error @ #{Time.new.strftime('%Y-%m-%dT%H:%M:%S')}"
43
+ STDERR.puts "#{e.inspect}"
44
+ STDERR.puts e.backtrace
45
+ end
11
46
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomasz Maczukin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-17 00:00:00.000000000 Z
11
+ date: 2015-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -80,25 +80,23 @@ files:
80
80
  - Rakefile
81
81
  - backup_example.rb
82
82
  - lib/simple_backup.rb
83
+ - lib/simple_backup/backend/abstract.rb
84
+ - lib/simple_backup/backend/local.rb
85
+ - lib/simple_backup/backends.rb
83
86
  - lib/simple_backup/dsl.rb
84
87
  - lib/simple_backup/engine.rb
85
- - lib/simple_backup/engine/abstract.rb
86
- - lib/simple_backup/engine/app_strategy/abstract.rb
87
- - lib/simple_backup/engine/app_strategy/bare.rb
88
- - lib/simple_backup/engine/app_strategy/capistrano.rb
89
- - lib/simple_backup/engine/app_strategy/factory.rb
90
- - lib/simple_backup/engine/apps.rb
91
- - lib/simple_backup/engine/mysql.rb
92
- - lib/simple_backup/exception.rb
93
- - lib/simple_backup/exception/app_already_defined.rb
94
- - lib/simple_backup/exception/apps_dir_does_not_exists.rb
95
- - lib/simple_backup/exception/base.rb
96
- - lib/simple_backup/exception/cant_create_dir.rb
97
- - lib/simple_backup/exception/type_does_not_exists.rb
98
- - lib/simple_backup/logger.rb
99
- - lib/simple_backup/mailer.rb
100
- - lib/simple_backup/storage.rb
88
+ - lib/simple_backup/source/abstract.rb
89
+ - lib/simple_backup/source/dir.rb
90
+ - lib/simple_backup/source/dir_strategy/bare.rb
91
+ - lib/simple_backup/source/dir_strategy/capistrano.rb
92
+ - lib/simple_backup/source/file.rb
93
+ - lib/simple_backup/source/mysql.rb
94
+ - lib/simple_backup/sources.rb
101
95
  - lib/simple_backup/utils.rb
96
+ - lib/simple_backup/utils/disk_usage.rb
97
+ - lib/simple_backup/utils/logger.rb
98
+ - lib/simple_backup/utils/mailer.rb
99
+ - lib/simple_backup/utils/mysql.rb
102
100
  - lib/simple_backup/version.rb
103
101
  - simple_backup.gemspec
104
102
  homepage: https://github.com/tmaczukin/simple_backup
@@ -121,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
119
  version: '0'
122
120
  requirements: []
123
121
  rubyforge_project:
124
- rubygems_version: 2.4.5
122
+ rubygems_version: 2.2.2
125
123
  signing_key:
126
124
  specification_version: 4
127
125
  summary: Backup tool with simple DSL for configuration
@@ -1,42 +0,0 @@
1
- module SimpleBackup
2
- module Engine
3
- class Abstract
4
- @keep_last = 5
5
-
6
- def storage=(storage)
7
- @storage = storage.space(self.class.name.split('::').last)
8
- end
9
-
10
- def keep_last(value)
11
- @keep_last = value.to_i
12
- @keep_last = 5 unless @keep_last > 0
13
- end
14
-
15
- def cleanup
16
- path = @storage.dir.path
17
-
18
- backups = Dir.glob(File.join(path, '**/*.tar.gz')).map do |file|
19
- file if file.match(/.*\.tar\.gz/)
20
- end.compact.sort
21
-
22
- to_persist = backups
23
- to_persist = backups.slice(@keep_last * -1, @keep_last) if backups.length > @keep_last
24
- to_remove = backups - to_persist
25
-
26
- to_remove.each do |file|
27
- Logger::info "Removing old backup #{file}"
28
- FileUtils.rm(file)
29
- end
30
- end
31
-
32
- def sources
33
- raise NotImplementedError
34
- end
35
-
36
- def backup
37
- raise NotImplementedError
38
- end
39
- end
40
- end
41
- end
42
-
@@ -1,13 +0,0 @@
1
- module SimpleBackup
2
- module Engine
3
- module AppStrategy
4
- class Abstract
5
- attr_accessor :storage
6
-
7
- def backup(name, path, attr)
8
- raise NotImplementedError
9
- end
10
- end
11
- end
12
- end
13
- end