backup-remote 0.0.3 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c0aa4fc4049cb34499a58fe72e4ebe283b950063
4
- data.tar.gz: c6b09225ac5faf3de22f73cf63fc801400cdfa3d
3
+ metadata.gz: c2fc3569f37e204350b31fc06bf5edde0f15a89e
4
+ data.tar.gz: dd9db17395b314a876850f35aecc9bea56ea5c0d
5
5
  SHA512:
6
- metadata.gz: 9c7162a70d86ae53879c7733a479985c34fcaea433dc2fcf1ed2ed2db26c51bfece25f6970fc10166d4ce0b5966a267089e13b473c66c8168aff8c5c7565e017
7
- data.tar.gz: 52b99fefd11d57b4a03b6dca9744c7d26e605fc8f8db5b30dd4ea988a2735d43359659c2af5025861f9724f60edc6f32794fa513c469201e9f3855e21db240f0
6
+ metadata.gz: 7e6bdac05bd47da6f855508e369576f59011058ea55cccad9b856b0514158786730a6207d1f0be31371c9fd7055913c1a8d7e5688c0fce0f74fdecec050b8a99
7
+ data.tar.gz: a40f18efe432ad1e45297e04549070435a6d487e55bc673e44fdb00ca3cf17a143c031bdbf95bf0c1a780f0140d0d7e48e58b9233ff7f515b4f5f87093456609
data/README.md CHANGED
@@ -12,6 +12,12 @@ Backup is a system utility for Linux and Mac OS X, distributed as a RubyGem, tha
12
12
  operations. It provides an elegant DSL in Ruby for _modeling_ your backups.
13
13
  Backup has built-in support for various databases, storage protocols/services, syncers, compressors, encryptors and notifiers which you can mix and match.
14
14
 
15
+ The gem adds the following model components:
16
+ * Remote Archive
17
+ * Remote MySQL database
18
+ * Remote data
19
+
20
+
15
21
 
16
22
 
17
23
  # How it works
@@ -67,11 +73,34 @@ Options for SSH connection:
67
73
 
68
74
  ## Archive files on a remote server
69
75
 
70
- * Use RemoteArchive
76
+ * Use remote_archive in your model
77
+
71
78
 
72
79
  ```
80
+
81
+ Model.new(:my_server_files_backup, 'Backup files') do
82
+
83
+ remote_archive :files do |archive|
84
+ archive.server_host = "myserver.com"
85
+ archive.server_ssh_user = "user"
86
+ archive.server_ssh_password = "pwd"
87
+
88
+
89
+ # archive options - the same options as for archive
90
+ # see http://backup.github.io/backup/v4/archives/
91
+
92
+
93
+ end
94
+
95
+ ...
96
+
97
+ end
98
+
73
99
  ```
74
100
 
101
+ Options:
102
+ * server_command - command to create archive file
103
+
75
104
 
76
105
  # Databases
77
106
 
@@ -80,6 +109,7 @@ Options for SSH connection:
80
109
  * Now it is implemented the following databases:
81
110
  * RemoteMySQL
82
111
 
112
+
83
113
  ### RemoteMySQL
84
114
 
85
115
  ```
@@ -97,13 +127,45 @@ Model.new(:my_backup, 'My Backup') do
97
127
  ...
98
128
  end
99
129
  ..
100
- end
101
-
130
+ end
131
+
102
132
  ````
103
133
 
104
- # Custom backup command
105
134
 
106
- * Run custom command to create a backup archive on a remote server
135
+
136
+ # Custom data on remote server
137
+
138
+ * Run custom command on the remote server to create a backup archive
139
+
140
+ * Specify command to run to generate archive file on the remote server
141
+
142
+ * This command should create an archive file with filename specified in server_path option.
143
+
144
+
145
+ ```
146
+ Model.new(:my_server_data_backup, 'Backup data') do
147
+
148
+ remote_data :mydata do |archive|
149
+ archive.server_host = "myserver.com"
150
+ archive.server_ssh_user = "user"
151
+ archive.server_ssh_password = "pwd"
152
+
153
+
154
+ archive.command = "--any command to generate backup archive file--"
155
+ # archive.command = "echo '1' > /tmp/backup.txt"
156
+
157
+ archive.server_path = "/path/to/archive.tar.gz"
158
+ # archive.command = "/tmp/backup.txt"
159
+
160
+
161
+
162
+ end
163
+
164
+ ...
165
+
166
+ end
167
+
168
+ ```
107
169
 
108
170
 
109
171
  # Backup gem
data/lib/backup.rb CHANGED
@@ -149,7 +149,13 @@ module Backup
149
149
  pipeline
150
150
  splitter
151
151
  template
152
+
153
+ remote_archive
154
+ remote_data
155
+
152
156
  version
153
- }.each {|lib| require File.join(LIBRARY_PATH, lib) }
157
+ }.each do |lib|
158
+ require File.join(LIBRARY_PATH, lib)
159
+ end
154
160
 
155
161
  end
data/lib/backup/model.rb CHANGED
@@ -139,6 +139,19 @@ module Backup
139
139
  @archives << Archive.new(self, name, &block)
140
140
  end
141
141
 
142
+ ##
143
+ # Adds a Remote Archive. Multiple archives may be added to the model.
144
+ def remote_archive(name, &block)
145
+ @archives << RemoteArchive.new(self, name, &block)
146
+ end
147
+
148
+ ##
149
+ # Adds a Remote data Archive
150
+ def remote_data(name, &block)
151
+ @archives << RemoteData.new(self, name, &block)
152
+ end
153
+
154
+
142
155
  ##
143
156
  # Adds an Database. Multiple Databases may be added to the model.
144
157
  def database(name, database_id = nil, &block)
@@ -77,6 +77,39 @@ module Backup
77
77
  }
78
78
  end
79
79
 
80
+ def ssh_upload_file(hostname, ssh_user, ssh_pass, source_file, dest_file, handler=nil)
81
+ host = SSHKit::Host.new("#{ssh_user}@#{hostname}")
82
+ host.password = ssh_pass
83
+
84
+ # scp
85
+ f_temp = "/tmp/#{SecureRandom.uuid}"
86
+
87
+ # sshkit
88
+ on host do |host|
89
+ as(user: ssh_user) do
90
+
91
+ end
92
+
93
+ # NOT WORK with sudo
94
+ #upload! source_file, dest_file
95
+
96
+ # upload to temp file
97
+ upload! source_file, f_temp
98
+
99
+ # upload to dest
100
+ execute("cp #{f_temp} #{dest_file}", interaction_handler: handler)
101
+
102
+ end
103
+
104
+ #
105
+ return {res: 1, output: ""}
106
+ rescue => e
107
+ {
108
+ res: 0,
109
+ error: e.message
110
+ }
111
+ end
112
+
80
113
  end
81
114
  end
82
115
  end
@@ -0,0 +1,266 @@
1
+ # encoding: utf-8
2
+
3
+ require 'net/ssh'
4
+
5
+ require 'sshkit'
6
+ require 'sshkit/dsl'
7
+ require 'sshkit/sudo'
8
+
9
+
10
+ module Backup
11
+ class RemoteArchive < Archive
12
+ class Error < Backup::Error; end
13
+
14
+ include Utilities::Helpers
15
+ #attr_reader :name, :options
16
+
17
+ include SSHKit::DSL
18
+
19
+ # server options
20
+ attr_accessor :server_host
21
+ attr_accessor :server_ssh_user
22
+ attr_accessor :server_ssh_password
23
+ attr_accessor :server_ssh_key
24
+ attr_accessor :server_backup_path
25
+
26
+
27
+
28
+ ##
29
+ # Adds a new Archive to a Backup Model.
30
+ #
31
+ # Backup::Model.new(:my_backup, 'My Backup') do
32
+ # archive :my_archive do |archive|
33
+ # archive.add 'path/to/archive'
34
+ # archive.add '/another/path/to/archive'
35
+ # archive.exclude 'path/to/exclude'
36
+ # archive.exclude '/another/path/to/exclude'
37
+ # end
38
+ # end
39
+ #
40
+ # All paths added using `add` or `exclude` will be expanded to their
41
+ # full paths from the root of the filesystem. Files will be added to
42
+ # the tar archive using these full paths, and their leading `/` will
43
+ # be preserved (using tar's `-P` option).
44
+ #
45
+ # /path/to/pwd/path/to/archive/...
46
+ # /another/path/to/archive/...
47
+ #
48
+ # When a `root` path is given, paths to add/exclude are taken as
49
+ # relative to the `root` path, unless given as absolute paths.
50
+ #
51
+ # Backup::Model.new(:my_backup, 'My Backup') do
52
+ # archive :my_archive do |archive|
53
+ # archive.root '~/my_data'
54
+ # archive.add 'path/to/archive'
55
+ # archive.add '/another/path/to/archive'
56
+ # archive.exclude 'path/to/exclude'
57
+ # archive.exclude '/another/path/to/exclude'
58
+ # end
59
+ # end
60
+ #
61
+ # This directs `tar` to change directories to the `root` path to create
62
+ # the archive. Unless paths were given as absolute, the paths within the
63
+ # archive will be relative to the `root` path.
64
+ #
65
+ # path/to/archive/...
66
+ # /another/path/to/archive/...
67
+ #
68
+ # For absolute paths added to this archive, the leading `/` will be
69
+ # preserved. Take note that when archives are extracted, leading `/` are
70
+ # stripped by default, so care must be taken when extracting archives with
71
+ # mixed relative/absolute paths.
72
+ def initialize(model, name, &block)
73
+ @model = model
74
+ @name = name.to_s
75
+ @options = {
76
+ :sudo => false,
77
+ :root => false,
78
+ :paths => [],
79
+ :excludes => [],
80
+ :tar_options => ''
81
+ }
82
+
83
+ DSL.new(@options).instance_eval(&block)
84
+
85
+ #
86
+ self.server_host = @options[:server_host]
87
+ self.server_ssh_user = @options[:server_ssh_user]
88
+ self.server_ssh_password = @options[:server_ssh_password]
89
+ end
90
+
91
+ def perform!
92
+ Logger.info "Creating Archive '#{ name }'..."
93
+
94
+ #
95
+ path = File.join(Config.tmp_path, @model.trigger, 'archives')
96
+ FileUtils.mkdir_p(path)
97
+
98
+
99
+ #
100
+ remote = Backup::Remote::Command.new
101
+
102
+ pipeline = Pipeline.new
103
+ with_files_from(paths_to_package) do |files_from|
104
+ # upload to server
105
+ res_upload = remote.ssh_upload_file(server_host, server_ssh_user, server_ssh_password, files_from, files_from)
106
+
107
+ if res_upload[:res]==0
108
+ raise 'Cannot upload file from server - #{files_from}'
109
+ end
110
+
111
+ #
112
+ pipeline.add(
113
+ "#{ tar_command } #{ tar_options } -cPf -#{ tar_root } " +
114
+ "#{ paths_to_exclude } -T '#{ files_from }'",
115
+ tar_success_codes
116
+ )
117
+
118
+ extension = 'tar'
119
+ @model.compressor.compress_with do |command, ext|
120
+ pipeline << command
121
+ extension << ext
122
+ end if @model.compressor
123
+
124
+ #
125
+ archive_file = File.join(path, "#{ name }.#{ extension }")
126
+ remote_archive_file = File.join('/tmp', "#{ name }.#{ extension }")
127
+ pipeline << "#{ utility(:cat) } > '#{ remote_archive_file }'"
128
+
129
+
130
+ #pipeline.run
131
+
132
+ # generate backup on remote server
133
+ cmd_remote = pipeline.commands.join(" | ")
134
+
135
+ #puts "remote cmd: #{cmd_remote}"
136
+ #exit
137
+
138
+
139
+ res_generate = remote.run_ssh_cmd(server_host, server_ssh_user, server_ssh_password, cmd_remote)
140
+
141
+ if res_generate[:res]==0
142
+ raise 'Cannot create backup on server'
143
+ end
144
+
145
+ # download backup
146
+ res_download = remote.ssh_download_file(server_host, server_ssh_user, server_ssh_password, remote_archive_file, archive_file)
147
+
148
+ #puts "res: #{res_download}"
149
+
150
+ if res_download[:res]==0
151
+ raise 'Cannot download file from server'
152
+ end
153
+
154
+ # delete archive on server
155
+ res_delete = remote.run_ssh_cmd(server_host, server_ssh_user, server_ssh_password, "rm #{remote_archive_file}")
156
+
157
+ end
158
+
159
+ Logger.info "Archive '#{ name }' Complete!"
160
+
161
+ #if pipeline.success?
162
+ # Logger.info "Archive '#{ name }' Complete!"
163
+ #else
164
+ # raise Error, "Failed to Create Archive '#{ name }'\n" + pipeline.error_messages
165
+ #end
166
+ end
167
+
168
+ private
169
+
170
+ def tar_command
171
+ tar = utility(:tar)
172
+ options[:sudo] ? "#{ utility(:sudo) } -n #{ tar }" : tar
173
+ end
174
+
175
+ def tar_root
176
+ options[:root] ? " -C '#{ File.expand_path(options[:root]) }'" : ''
177
+ end
178
+
179
+ def paths_to_package
180
+ options[:paths].map {|path| prepare_path(path) }
181
+ end
182
+
183
+ def with_files_from(paths)
184
+ tmpfile = Tempfile.new('backup-archive-paths')
185
+ paths.each {|path| tmpfile.puts path }
186
+ tmpfile.close
187
+
188
+ puts "tmpfile #{tmpfile.path}"
189
+
190
+ puts "content: #{File.read(tmpfile.path)}"
191
+ #yield "-T '#{ tmpfile.path }'"
192
+ yield "#{ tmpfile.path }"
193
+ ensure
194
+
195
+ puts "delete file #{tmpfile.path}"
196
+ tmpfile.delete
197
+ end
198
+
199
+ def paths_to_exclude
200
+ options[:excludes].map {|path|
201
+ "--exclude='#{ prepare_path(path) }'"
202
+ }.join(' ')
203
+ end
204
+
205
+ def prepare_path(path)
206
+
207
+ res = options[:root] ? path : File.expand_path(path)
208
+
209
+ puts "path #{path} ===> #{res}"
210
+
211
+ res
212
+ end
213
+
214
+ def tar_options
215
+ args = options[:tar_options]
216
+ gnu_tar? ? "--ignore-failed-read #{ args }".strip : args
217
+ end
218
+
219
+ def tar_success_codes
220
+ gnu_tar? ? [0, 1] : [0]
221
+ end
222
+
223
+
224
+ ### DSL for RemoteArchive
225
+ class DSL
226
+ def initialize(options)
227
+ @options = options
228
+ end
229
+
230
+
231
+ ### remote server
232
+ def server_host=(val = true)
233
+ @options[:server_host] = val
234
+ end
235
+
236
+ def server_ssh_user=(val = true)
237
+ @options[:server_ssh_user] = val
238
+ end
239
+ def server_ssh_password=(val = true)
240
+ @options[:server_ssh_password] = val
241
+ end
242
+
243
+ ###
244
+ def use_sudo(val = true)
245
+ @options[:sudo] = val
246
+ end
247
+
248
+ def root(path)
249
+ @options[:root] = path
250
+ end
251
+
252
+ def add(path)
253
+ @options[:paths] << path
254
+ end
255
+
256
+ def exclude(path)
257
+ @options[:excludes] << path
258
+ end
259
+
260
+ def tar_options(opts)
261
+ @options[:tar_options] = opts
262
+ end
263
+ end
264
+
265
+ end
266
+ end
@@ -0,0 +1,237 @@
1
+ # encoding: utf-8
2
+
3
+ require 'net/ssh'
4
+
5
+ require 'sshkit'
6
+ require 'sshkit/dsl'
7
+ require 'sshkit/sudo'
8
+
9
+
10
+ module Backup
11
+ class RemoteData < Archive
12
+ class Error < Backup::Error; end
13
+
14
+ include Utilities::Helpers
15
+ #attr_reader :name, :options
16
+
17
+ include SSHKit::DSL
18
+
19
+ # server options
20
+ attr_accessor :server_host
21
+ attr_accessor :server_ssh_user
22
+ attr_accessor :server_ssh_password
23
+ attr_accessor :server_ssh_key
24
+ attr_accessor :server_path
25
+ attr_accessor :server_command
26
+
27
+
28
+
29
+
30
+
31
+ def initialize(model, name, &block)
32
+ @model = model
33
+ @name = name.to_s
34
+ @options = {
35
+ :sudo => false,
36
+ :root => false,
37
+ :paths => [],
38
+ :excludes => [],
39
+ :tar_options => ''
40
+ }
41
+
42
+ DSL.new(@options).instance_eval(&block)
43
+
44
+ #
45
+ self.server_host = @options[:server_host]
46
+ self.server_ssh_user = @options[:server_ssh_user]
47
+ self.server_ssh_password = @options[:server_ssh_password]
48
+ self.server_path = @options[:server_path]
49
+ self.server_command = @options[:server_command]
50
+ end
51
+
52
+ def perform!
53
+ Logger.info "Creating Archive '#{ name }'..."
54
+
55
+ # local archive
56
+ path = File.join(Config.tmp_path, @model.trigger, 'archives')
57
+ FileUtils.mkdir_p(path)
58
+
59
+ extension = 'tar'
60
+ #temp_archive_file = File.join(path, "#{ name }.#{ extension }")
61
+
62
+ remote = Backup::Remote::Command.new
63
+
64
+
65
+ Dir.mktmpdir do |temp_dir|
66
+ temp_local_file = File.join("#{temp_dir}", File.basename(server_path))
67
+ #temp_local_file = File.join(path, File.basename(server_path))
68
+ #temp_local_file = Tempfile.new("").path+"."+File.extname(server_path)
69
+
70
+ remote_archive_file = server_path
71
+
72
+ # generate backup on remote server
73
+ cmd_remote = server_command
74
+ res_generate = remote.run_ssh_cmd(
75
+ server_host, server_ssh_user, server_ssh_password,
76
+ cmd_remote)
77
+
78
+ if res_generate[:res]==0
79
+ raise 'Cannot create backup on server'
80
+ end
81
+
82
+ # download backup
83
+ res_download = remote.ssh_download_file(
84
+ server_host, server_ssh_user, server_ssh_password,
85
+ remote_archive_file, temp_local_file)
86
+
87
+ if res_download[:res]==0
88
+ raise 'Cannot download file from server'
89
+ end
90
+
91
+ # delete archive on server
92
+ res_delete = remote.run_ssh_cmd(
93
+ server_host, server_ssh_user, server_ssh_password,
94
+ "rm #{remote_archive_file}")
95
+
96
+
97
+ # process archive locally
98
+
99
+ pipeline = Pipeline.new
100
+
101
+ #temp_tar_root= tar_root
102
+ temp_tar_root= temp_dir
103
+ pipeline.add(
104
+ "#{ tar_command } #{ tar_options } -cPf - -C #{temp_tar_root } #{ File.basename(temp_local_file) }",
105
+ tar_success_codes
106
+ )
107
+
108
+ extension = 'tar'
109
+ @model.compressor.compress_with do |command, ext|
110
+ pipeline << command
111
+ extension << ext
112
+ end if @model.compressor
113
+
114
+ pipeline << "#{ utility(:cat) } > '#{ File.join(path, "#{ name }.#{ extension }") }'"
115
+
116
+ #puts "commands: #{pipeline.commands}"
117
+ #exit
118
+
119
+ pipeline.run
120
+
121
+
122
+ if pipeline.success?
123
+ Logger.info "Archive '#{ name }' Complete!"
124
+ else
125
+ raise Error, "Failed to Create Archive '#{ name }'\n" +
126
+ pipeline.error_messages
127
+ end
128
+
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def tar_command
135
+ tar = utility(:tar)
136
+ options[:sudo] ? "#{ utility(:sudo) } -n #{ tar }" : tar
137
+ end
138
+
139
+ def tar_root
140
+ options[:root] ? " -C '#{ File.expand_path(options[:root]) }'" : ''
141
+ end
142
+
143
+ def paths_to_package
144
+ options[:paths].map {|path| prepare_path(path) }
145
+ end
146
+
147
+ def with_files_from(paths)
148
+ tmpfile = Tempfile.new('backup-archive-paths')
149
+ paths.each {|path| tmpfile.puts path }
150
+ tmpfile.close
151
+
152
+ puts "tmpfile #{tmpfile.path}"
153
+
154
+ puts "content: #{File.read(tmpfile.path)}"
155
+ #yield "-T '#{ tmpfile.path }'"
156
+ yield "#{ tmpfile.path }"
157
+ ensure
158
+
159
+ puts "delete file #{tmpfile.path}"
160
+ tmpfile.delete
161
+ end
162
+
163
+ def paths_to_exclude
164
+ options[:excludes].map {|path|
165
+ "--exclude='#{ prepare_path(path) }'"
166
+ }.join(' ')
167
+ end
168
+
169
+ def prepare_path(path)
170
+
171
+ res = options[:root] ? path : File.expand_path(path)
172
+
173
+ puts "path #{path} ===> #{res}"
174
+
175
+ res
176
+ end
177
+
178
+ def tar_options
179
+ args = options[:tar_options]
180
+ gnu_tar? ? "--ignore-failed-read #{ args }".strip : args
181
+ end
182
+
183
+ def tar_success_codes
184
+ gnu_tar? ? [0, 1] : [0]
185
+ end
186
+
187
+
188
+ ### DSL for RemoteArchive
189
+ class DSL
190
+ def initialize(options)
191
+ @options = options
192
+ end
193
+
194
+
195
+ ### remote server
196
+ def server_host=(val = true)
197
+ @options[:server_host] = val
198
+ end
199
+
200
+ def server_ssh_user=(val = true)
201
+ @options[:server_ssh_user] = val
202
+ end
203
+ def server_ssh_password=(val = true)
204
+ @options[:server_ssh_password] = val
205
+ end
206
+
207
+ def server_command=(val = true)
208
+ @options[:server_command] = val
209
+ end
210
+ def server_path=(val = true)
211
+ @options[:server_path] = val
212
+ end
213
+
214
+ ###
215
+ def use_sudo(val = true)
216
+ @options[:sudo] = val
217
+ end
218
+
219
+ def root(path)
220
+ @options[:root] = path
221
+ end
222
+
223
+ def add(path)
224
+ @options[:paths] << path
225
+ end
226
+
227
+ def exclude(path)
228
+ @options[:excludes] << path
229
+ end
230
+
231
+ def tar_options(opts)
232
+ @options[:tar_options] = opts
233
+ end
234
+ end
235
+
236
+ end
237
+ end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Backup
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.5'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backup-remote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Ivak, Michael van Rooijen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-14 00:00:00.000000000 Z
11
+ date: 2016-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: CFPropertyList
@@ -842,14 +842,14 @@ dependencies:
842
842
  requirements:
843
843
  - - '='
844
844
  - !ruby/object:Gem::Version
845
- version: 0.18.1
845
+ version: 0.19.1
846
846
  type: :runtime
847
847
  prerelease: false
848
848
  version_requirements: !ruby/object:Gem::Requirement
849
849
  requirements:
850
850
  - - '='
851
851
  - !ruby/object:Gem::Version
852
- version: 0.18.1
852
+ version: 0.19.1
853
853
  - !ruby/object:Gem::Dependency
854
854
  name: thread_safe
855
855
  requirement: !ruby/object:Gem::Requirement
@@ -1017,6 +1017,8 @@ files:
1017
1017
  - lib/backup/packager.rb
1018
1018
  - lib/backup/pipeline.rb
1019
1019
  - lib/backup/remote/command.rb
1020
+ - lib/backup/remote_archive.rb
1021
+ - lib/backup/remote_data.rb
1020
1022
  - lib/backup/splitter.rb
1021
1023
  - lib/backup/storage/base.rb
1022
1024
  - lib/backup/storage/cloud_files.rb