lsync 2.1.0 → 2.3.1

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.
@@ -1,3 +1,22 @@
1
+ # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  module LSync
3
22
 
@@ -1,3 +1,22 @@
1
+ # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  module LSync
3
22
 
@@ -12,13 +31,21 @@ module LSync
12
31
  end
13
32
 
14
33
  # Fire an event which calls all registered event handlers in the order they were defined.
34
+ # The first argument is used to #instance_eval any handlers.
15
35
  def fire(event, *args)
16
36
  handled = false
17
37
 
38
+ scope = args.shift
39
+
18
40
  if @events && @events[event]
19
41
  @events[event].each do |handler|
20
42
  handled = true
21
- handler.call(*args)
43
+
44
+ if scope
45
+ scope.instance_eval &handler
46
+ else
47
+ handler.call
48
+ end
22
49
  end
23
50
  end
24
51
 
@@ -1,3 +1,22 @@
1
+ # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  require 'thread'
3
22
 
@@ -1,3 +1,22 @@
1
+ # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  require 'fileutils'
3
22
  require 'pathname'
@@ -11,15 +30,12 @@ module LSync
11
30
  include EventHandler
12
31
 
13
32
  def initialize(options = {})
14
- @logger = options[:logger] || Logger.new(STDOUT)
15
33
  end
16
34
 
17
- attr :logger, true
18
-
19
- def run(master_server, target_server, directory)
35
+ def run(controller)
20
36
  end
21
37
 
22
- def should_run?(master_server, current_server, target_server)
38
+ def should_run?(controller)
23
39
  end
24
40
  end
25
41
 
@@ -1,3 +1,22 @@
1
+ # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  require 'lsync/method'
3
22
 
@@ -24,63 +43,64 @@ module LSync
24
43
 
25
44
  def initialize(direction, options = {})
26
45
  super(options)
46
+
27
47
  @direction = direction
28
48
  @command = options[:command] || "rsync"
29
-
49
+
30
50
  @options = options
31
51
  @connection = nil
32
52
  end
33
53
 
34
- def run(master_server, target_server, directory)
54
+ def run(controller)
55
+ directory = controller.directory
35
56
  arguments = (@options[:arguments] || ["--archive"]) + (directory.options[:arguments] || [])
36
57
 
37
58
  local_server = nil
38
59
  remote_server = nil
39
60
 
40
61
  if @direction == :push
41
- local_server = master_server
42
- remote_server = target_server
62
+ local_server = controller.master
63
+ remote_server = controller.target
43
64
 
44
- dst = remote_server.connection_string(directory)
45
- src = local_server.full_path(directory)
65
+ destination = remote_server.connection_string(directory)
66
+ source = local_server.full_path(directory)
46
67
  else
47
- local_server = target_server
48
- remote_server = master_server
68
+ local_server = controller.target
69
+ remote_server = controller.master
49
70
 
50
- src = remote_server.connection_string(directory)
51
- dst = local_server.full_path(directory)
71
+ source = remote_server.connection_string(directory)
72
+ destination = local_server.full_path(directory)
52
73
  end
53
74
 
54
75
  arguments += connect_arguments(local_server, remote_server)
55
76
 
56
77
  # Create the destination backup directory
57
- @connection = target_server.connect
58
- @connection.send_object([:mkdir_p, target_server.full_path(directory)])
78
+ controller.target.exec!(["mkdir", "-p", controller.target.full_path(directory.path)])
59
79
 
60
- @logger.info "In directory #{Dir.getwd}..."
80
+ controller.logger.info "In directory #{Dir.getwd}..."
61
81
  Dir.chdir(local_server.root) do
62
- if run_handler(src, dst, arguments) == false
63
- raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
82
+ if run_handler(controller, source, destination, arguments) == false
83
+ raise BackupMethodError.new("Backup from #{source} to #{destination} failed.", :method => self)
64
84
  end
65
85
  end
66
86
  end
67
87
 
68
- def run_handler(src, dst, arguments)
69
- run_command([@command] + arguments + [src, dst])
88
+ def run_handler(controller, source, destination, arguments)
89
+ run_command(controller, [@command] + arguments + [source, destination])
70
90
  end
71
91
 
72
- def should_run?(master_server, current_server, target_server)
92
+ def should_run?(controller)
73
93
  if @direction == :push
74
- return current_server == master_server
94
+ return controller.current == controller.master
75
95
  elsif @direction == :pull
76
- return target_server.is_local?
96
+ return controller.target.local?
77
97
  else
78
98
  return false
79
99
  end
80
100
  end
81
101
 
82
- def run_command(cmd)
83
- return LSync.run_command(cmd, @logger) == 0
102
+ def run_command(controller, command)
103
+ return LSync.run_command("/", command, controller.logger) == 0
84
104
  end
85
105
  end
86
106
 
@@ -89,40 +109,40 @@ module LSync
89
109
  @options[:inprogress_path] || "backup.inprogress"
90
110
  end
91
111
 
92
- def run(master_server, target_server, directory)
112
+ def run(controller)
113
+ directory = controller.directory
93
114
  arguments = (@options[:arguments] || []) + (directory.options[:arguments] || [])
94
115
 
95
116
  link_dest = Pathname.new("../" * (directory.path.depth + 1)) + "latest" + directory.path
96
117
  arguments += ['--archive', '--link-dest', link_dest.to_s]
97
118
 
98
- dst_directory = File.join(inprogress_path, directory.to_s)
119
+ destination_directory = File.join(inprogress_path, directory.path)
99
120
 
100
121
  local_server = nil
101
122
  remote_server = nil
102
123
 
103
124
  if @direction == :push
104
- local_server = master_server
105
- remote_server = target_server
125
+ local_server = controller.master
126
+ remote_server = controller.target
106
127
 
107
- dst = remote_server.connection_string(dst_directory)
108
- src = local_server.full_path(directory)
128
+ destination = remote_server.connection_string(destination_directory)
129
+ source = local_server.full_path(directory)
109
130
  else
110
- local_server = target_server
111
- remote_server = master_server
131
+ local_server = controller.target
132
+ remote_server = controller.master
112
133
 
113
- dst = local_server.full_path(dst_directory)
114
- src = remote_server.connection_string(directory)
134
+ destination = local_server.full_path(destination_directory)
135
+ source = remote_server.connection_string(directory)
115
136
  end
116
137
 
117
138
  arguments += connect_arguments(local_server, remote_server)
118
139
 
119
140
  # Create the destination backup directory
120
- @connection = target_server.connect
121
- @connection.send_object([:mkdir_p, target_server.full_path(dst_directory)])
141
+ controller.target.exec!(["mkdir", "-p", controller.target.full_path(destination_directory)])
122
142
 
123
143
  Dir.chdir(local_server.root) do
124
- if run_handler(src, dst, arguments) == false
125
- raise BackupMethodError.new("Backup from #{src.dump} to #{dst.dump} failed.", :method => self)
144
+ if run_handler(controller, source, destination, arguments) == false
145
+ raise BackupMethodError.new("Backup from #{source} to #{destination} failed.", :method => self)
126
146
  end
127
147
  end
128
148
  end
@@ -1,35 +1,71 @@
1
+ # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  require 'rexec/task'
3
22
 
4
23
  module LSync
5
24
 
6
- # Run a specific command and output the results to the given logger.
7
- def self.run_command(command, logger)
8
- logger.info "Running: #{command.to_cmd} in #{Dir.getwd.dump}"
9
-
10
- process_result = RExec::Task.open(command) do |task|
11
- task.input.close
12
- pipes = [task.output, task.error]
13
-
14
- while pipes.size > 0
15
- result = IO.select(pipes)
16
-
17
- result[0].each do |pipe|
18
- if pipe.closed? || pipe.eof?
19
- pipes.delete(pipe)
20
- next
21
- end
22
-
23
- if pipe == task.output
24
- logger.info pipe.readline.chomp
25
- elsif pipe == task.error
26
- logger.error pipe.readline.chomp
27
- end
25
+ def self.log_task(task, logger)
26
+ pipes = [task.output, task.error]
27
+
28
+ while pipes.size > 0
29
+ result = IO.select(pipes)
30
+
31
+ result[0].each do |pipe|
32
+ if pipe.closed? || pipe.eof?
33
+ pipes.delete(pipe)
34
+ next
35
+ end
36
+
37
+ if pipe == task.output
38
+ logger.info pipe.readline.chomp
39
+ elsif pipe == task.error
40
+ logger.error pipe.readline.chomp
28
41
  end
29
42
  end
30
43
  end
44
+ end
31
45
 
32
- return process_result
46
+ # Run a specific command and output the results to the given logger.
47
+ def self.run_command(root, command, logger)
48
+ Dir.chdir(root) do
49
+ logger.info "Running #{command.inspect} in #{Dir.getwd.inspect}"
50
+
51
+ process_result = RExec::Task.open(command) do |task|
52
+ log_task(task, logger)
53
+ end
54
+
55
+ return process_result
56
+ end
33
57
  end
34
58
 
59
+ def self.run_remote_command(root, connection_command, command, logger)
60
+ logger.info "Running remote command #{command.inspect} in #{root}"
61
+
62
+ process_result = RExec::Task.open(connection_command) do |connection|
63
+ connection.puts(["cd", root].to_cmd)
64
+ connection.puts((["exec"] + command).to_cmd)
65
+
66
+ LSync::log_task(connection, logger)
67
+ end
68
+
69
+ return process_result
70
+ end
35
71
  end
@@ -1,7 +1,23 @@
1
+ # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
- # A backup script coordinates "one" backup as a unit.
3
-
4
- require 'lsync/action'
5
21
  require 'lsync/method'
6
22
  require 'lsync/server'
7
23
  require 'lsync/directory'
@@ -15,11 +31,18 @@ module LSync
15
31
  include EventHandler
16
32
 
17
33
  def initialize(options = {}, &block)
18
- @logger = options[:logger] || Logger.new($stdout)
34
+ if options[:logger]
35
+ @logger = options[:logger]
36
+ else
37
+ @logger = Logger.new($stdout)
38
+ @logger.formatter = LSync::MinimalLogFormat.new
39
+ end
40
+
19
41
  @method = nil
20
42
 
21
43
  @servers = {}
22
44
  @directories = []
45
+ @master = "localhost"
23
46
 
24
47
  @log = nil
25
48
 
@@ -34,7 +57,7 @@ module LSync
34
57
  return @servers[name]
35
58
  else
36
59
  hostname = Socket.gethostbyname(name)[0] rescue name
37
- return @servers.values.find { |s| s["host"] == hostname }
60
+ return @servers.values.find { |s| s.host == hostname }
38
61
  end
39
62
  end
40
63
 
@@ -51,11 +74,11 @@ module LSync
51
74
  server = nil
52
75
 
53
76
  # Find out if the master server is local...
54
- if master.is_local?
77
+ if master && master.local?
55
78
  server = master
56
79
  else
57
80
  # Find a server config that specifies the local host
58
- server = @servers.values.find { |s| s.is_local? }
81
+ server = @servers.values.find { |s| s.local? }
59
82
  end
60
83
 
61
84
  return server
@@ -109,7 +132,7 @@ module LSync
109
132
  attr :log
110
133
 
111
134
  # Run the backup process for all servers and directories specified.
112
- def run!
135
+ def run!(options = {})
113
136
  start_time = Time.now
114
137
 
115
138
  # We buffer the log data so that if there is an error it is available to the notification sub-system
@@ -126,7 +149,7 @@ module LSync
126
149
  raise ScriptError.new("Could not determine current server!", :script => self, :master => @master)
127
150
  end
128
151
 
129
- if master.is_local?
152
+ if master.local?
130
153
  logger.info "We are the master server..."
131
154
  else
132
155
  logger.info "We are not the master server..."
@@ -137,42 +160,48 @@ module LSync
137
160
 
138
161
  self.try do
139
162
  method.try do
140
- master.try(master_controller) do
141
- logger.info "Running backups for server #{current}..."
163
+ logger.info "Running backups for server #{current}..."
142
164
 
143
- run_backups!(master, current, logger)
144
- end
165
+ run_backups!(master, current, logger, options)
145
166
  end
146
167
  end
147
168
 
148
169
  end_time = Time.now
149
- logger.info "Backup Completed (#{end_time - start_time}s)."
170
+ logger.info "===== Finished ====="
171
+ logger.info "[Time]: (#{end_time - start_time}s)."
150
172
  end
151
173
 
152
174
  protected
153
175
 
154
176
  # This function runs the method for each directory and server combination specified.
155
- def run_backups!(master, current, logger)
177
+ def run_backups!(master, current, logger, options = {})
156
178
  @servers.each do |name, server|
157
179
  # S is always a data destination, therefore s can't be @master
158
180
  next if server == master
159
181
 
182
+ next unless server.role?(options[:role] || :any)
183
+
184
+ server_controller = CopyController.new(self, logger, master, server, current)
185
+
160
186
  # Skip servers that shouldn't be processed
161
- unless @method.should_run?(master, current, server)
162
- logger.info "\t" + "Skipping".rjust(20) + " : #{s}"
187
+ unless @method.should_run?(server_controller)
188
+ logger.info "===== Skipping ====="
189
+ logger.info "[Master]: #{master}"
190
+ logger.info "[Target]: #{server}"
163
191
  next
164
192
  end
165
193
 
166
- server_controller = CopyController.new(self, logger, master, server)
167
-
194
+ logger.info "===== Processing ====="
195
+ logger.info "[Master]: #{master}"
196
+ logger.info "[Target]: #{server}"
197
+
168
198
  server.try(server_controller) do
169
199
  @directories.each do |directory|
170
- directory_controller = DirectoryController.new(self, logger, master, server, directory)
200
+ directory_controller = DirectoryController.new(self, logger, master, server, current, directory)
171
201
 
202
+ logger.info "[Directory]: #{directory}"
172
203
  directory.try(directory_controller) do
173
- logger.info "\t" + ("Processing " + directory.to_s).rjust(20) + " : #{server}"
174
-
175
- method.run(master, server, directory)
204
+ method.run(directory_controller)
176
205
  end
177
206
  end
178
207
  end