backup-pcs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 049ec16812b9a9610beaf7dcbb21fff5cfeb4072
4
+ data.tar.gz: 42ae0e0c689a2a90e70a64ebdad97b49181664b9
5
+ SHA512:
6
+ metadata.gz: 48a5abf2774d5820571acf27583bf50fbeb9b897625ae363f5a366102b6fcbf143a1717f870843c4d62ec3111936d3bc2df50ac25da28fb7b2687d454eec1c8d
7
+ data.tar.gz: b592117bfd34d9cfeb38002898a84e9fcc99366b811b60f200daf8dc7d6993f66b1839b6f694a568234683a7ba9889da211b7048fb2768b3dd4a30c841a8a5f0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ bundler_args: --without development
3
+ script: 'bundle exec rake spec'
4
+
5
+ rvm:
6
+ - 1.9.3
7
+ - 2.0.0
8
+ - rbx-2.1.1
9
+ - jruby-19mode
10
+ - ruby-head
11
+
12
+ matrix:
13
+ allow_failures:
14
+ - rvm: rbx-2.1.1
15
+ - rvm: jruby-19mode
16
+ - rvm: ruby-head
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'guard-rspec'
5
+ gem 'terminal-notifier-guard' if /darwin/ =~ RUBY_PLATFORM
6
+ end
7
+
8
+ group :test, :development do
9
+ gem 'rake'
10
+ gem 'rspec'
11
+ end
12
+
13
+ gem 'rubysl', platforms: :rbx
14
+
15
+ # Specify your gem's dependencies in backup-pcs.gemspec
16
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard :rspec, all_on_start: true, all_after_pass: true do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Lonre Wang
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Baidu PCS Storage for Backup [![Build Status](https://travis-ci.org/lonre/backup-pcs.png?branch=master)](https://travis-ci.org/lonre/backup-pcs)
2
+
3
+ `PCS(Personal Cloud Storage)`是百度推出的针对个人用户的云存储服务
4
+
5
+ 本 Gem 为 [Backup](https://github.com/meskyanichi/backup) Storage 插件,可以将数据备份到 `PCS`
6
+
7
+ 使用 `backup-pcs` 前必须到 [百度开发者中心](http://developer.baidu.com/console) 开启指定应用的 PCS API 权限,参考 [开通PCS API权限](http://developer.baidu.com/wiki/index.php?title=docs/pcs/guide/api_approve),并获取所设置的文件目录
8
+
9
+ ## 安装
10
+
11
+ ```
12
+ $ gem install backup-pcs
13
+ ```
14
+ `backup-pcs` 当前支持 Ruby(MRI) 版本:1.9.3, 2.0.0
15
+
16
+ ## 用法
17
+
18
+ 此说明只提供了本 Gem 的配置使用方法,关于 Backup 的详细使用方法,请参考 [Backup GitHub Page](https://github.com/meskyanichi/backup)
19
+
20
+ 用你喜欢的文本编辑器,打开 Backup model 文件,如:`~/Backup/models/mysite.rb`
21
+
22
+ ### 引入依赖
23
+
24
+ 在头部添加依赖
25
+
26
+ ```ruby
27
+ require 'backup/pcs'
28
+ ```
29
+
30
+ ### 配置
31
+
32
+ 在主体部分添加如下配置:
33
+
34
+ ```ruby
35
+ store_with :PCS do |p|
36
+ p.client_id = 'a_client_id'
37
+ p.client_secret = 'a_cliet_secret'
38
+ p.dir_name = 'Backups' # 开通 PCS API 权限时所设置的文件目录
39
+ p.path = 'data' # 保存路径,从 dir_name 算起
40
+ # p.keep = 2
41
+ # p.max_retries = 10 # 出错后重试次数,默认 10 次
42
+ # p.retry_waitsec = 30 # 出错后重试等待秒数,默认 30 秒
43
+ end
44
+ ```
45
+
46
+ ### 注意
47
+
48
+ 在配置完成之后,您必须手动在系统中运行一次 `Backup`,根据终端的提示完成授权。在授权完毕之后,授权信息会被缓存,这样,当后续自动任务触发 `Backup` 时,会自动加载此缓存授权信息。
49
+
50
+ ## Copyright
51
+ MIT License. Copyright (c) 2013 Lonre Wang.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'backup/pcs/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "backup-pcs"
8
+ spec.version = Backup::PCS::VERSION
9
+ spec.authors = ["Lonre Wang"]
10
+ spec.email = ["me@wanglong.me"]
11
+ spec.description = %q{Backup Storage for supporting Baidu Personal Cloud Storage(PCS)}
12
+ spec.summary = %q{Baidu PCS Storage for Backup}
13
+ spec.homepage = "https://github.com/lonre/backup-pcs"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "baidu-sdk", "~> 0.0.1"
22
+ spec.add_runtime_dependency "backup", "~> 3.9.0"
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ end
@@ -0,0 +1,5 @@
1
+ module Backup
2
+ module PCS
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
data/lib/backup/pcs.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "backup"
2
+
3
+ Backup::Storage.autoload(:PCS, File.join(File.dirname(__FILE__), 'storage/pcs'))
@@ -0,0 +1,131 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'baidu/oauth'
4
+ require 'baidu/pcs'
5
+
6
+ module Backup
7
+ module Storage
8
+ class PCS < Base
9
+ class Error < Backup::Error; end
10
+
11
+ attr_accessor :client_id, :client_secret, :dir_name, :max_retries, :retry_waitsec
12
+
13
+ def initialize(model, storage_id=nil)
14
+ super
15
+
16
+ @path ||= 'backups'
17
+ @max_retries ||= 10
18
+ @retry_waitsec ||= 30
19
+ end
20
+
21
+ private
22
+
23
+ def transfer!
24
+ package.filenames.each do |filename|
25
+ src = File.join(Config.tmp_path, filename)
26
+ dest = File.join(remote_path, filename)
27
+ Logger.info "Storing '#{ dest }'..."
28
+ auto_refresh_token do
29
+ File.open(src, 'r') do |file|
30
+ client.upload file, path: dest, block_upload: true,
31
+ retry_times: @max_retries,
32
+ retry_waitsec: @retry_waitsec
33
+ end
34
+ end
35
+ end
36
+ rescue => err
37
+ raise Error.wrap(err, 'Upload Failed!')
38
+ end
39
+
40
+ def remove!(package)
41
+ Logger.info "Removing backup package dated #{ package.time }..."
42
+
43
+ auto_refresh_token do
44
+ client.delete(remote_path_for(package))
45
+ end
46
+ end
47
+
48
+ # create or get Baidu PCS client
49
+ def client
50
+ return @client if @client
51
+
52
+ unless session = cached_session
53
+ Logger.info "Creating a new Baidu session!"
54
+ session = create_session!
55
+ end
56
+
57
+ @client = Baidu::PCS::Client.new(session, dir_name)
58
+ rescue => err
59
+ raise Error.wrap(err, 'Authorization Failed.')
60
+ end
61
+
62
+ # cache Baidu::Session
63
+ def cached_session
64
+ session = false
65
+ if File.exist?(cached_file)
66
+ begin
67
+ content = Base64.decode64(File.read(cached_file))
68
+ session = Marshal.load(content)
69
+ Logger.info "Baidu session data loaded from cache!"
70
+ rescue => err
71
+ Logger.warn Error.wrap(err, "Cached file: #{cached_file} might be corrupt.")
72
+ end
73
+ end
74
+ session
75
+ end
76
+
77
+ def auto_refresh_token
78
+ tries = 0
79
+ begin
80
+ yield
81
+ rescue Baidu::Errors::AuthError => e
82
+ raise Error, 'Too many auth errors' if (tries += 1) > 5
83
+ Logger.info "Refreshing Baidu session..."
84
+ session = oauth_client.refresh(cached_session.refresh_token)
85
+ raise Error.wrap(e, 'Authorization Failed!') unless session
86
+ write_cache! session
87
+ @client = nil
88
+ retry
89
+ end
90
+ end
91
+
92
+ def cached_file
93
+ File.join(Config.cache_path, "pcs_#{storage_id}_#{client_id}")
94
+ end
95
+
96
+ def write_cache!(session)
97
+ FileUtils.mkdir_p(Config.cache_path)
98
+ File.open(cached_file, "w") do |cache_file|
99
+ cache_file.write(Base64.encode64(Marshal.dump(session)))
100
+ end
101
+ end
102
+
103
+ def oauth_client
104
+ Baidu::OAuth::Client.new(client_id, client_secret)
105
+ end
106
+
107
+ def create_session!
108
+ require 'timeout'
109
+
110
+ device_flow = oauth_client.device_flow
111
+ rest = device_flow.user_and_device_code('netdisk')
112
+
113
+ STDOUT.puts "1. Visit verification url: #{rest[:verification_url]}"
114
+ STDOUT.puts "2. Type user code below in the form"
115
+ STDOUT.puts "\t #{rest[:user_code]}"
116
+ STDOUT.puts "3. Hit 'Enter/Return' once you're authorized."
117
+
118
+ Timeout::timeout(300) { STDIN.gets }
119
+
120
+ session = device_flow.get_token(rest[:device_code])
121
+
122
+ write_cache!(session)
123
+ STDOUT.puts "Baidu session cached: #{cached_file}"
124
+
125
+ session
126
+ rescue => err
127
+ raise Error.wrap(err, 'Could not create a new session!')
128
+ end
129
+ end
130
+ end
131
+ end
data/spec/pcs_spec.rb ADDED
@@ -0,0 +1,412 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'backup/pcs'
4
+
5
+ module Backup
6
+ describe Storage::PCS do
7
+ let(:model) { Model.new(:test_trigger, 'test label') }
8
+ let(:storage) { Storage::PCS.new(model) }
9
+
10
+ describe '#initialize' do
11
+ it 'is a Storage::Base' do
12
+ expect(storage).to be_a(Storage::Base)
13
+ end
14
+
15
+ it 'has default config' do
16
+ expect(storage.storage_id ).to be_nil
17
+ expect(storage.keep ).to be_nil
18
+ expect(storage.client_id ).to be_nil
19
+ expect(storage.client_secret).to be_nil
20
+ expect(storage.dir_name ).to be_nil
21
+ expect(storage.path ).to eq 'backups'
22
+ expect(storage.max_retries ).to be 10
23
+ expect(storage.retry_waitsec).to be 30
24
+ end
25
+
26
+ it 'inits with config' do
27
+ storage = Storage::PCS.new(model, 'sid') do |c|
28
+ c.keep = 1
29
+ c.client_id = 'ci'
30
+ c.client_secret = 'cs'
31
+ c.dir_name = 'dn'
32
+ c.path = 'myback'
33
+ c.max_retries = 2
34
+ c.retry_waitsec = 3
35
+ end
36
+
37
+ expect(storage.storage_id ).to eq('sid')
38
+ expect(storage.keep ).to eq(1)
39
+ expect(storage.client_id ).to eq('ci')
40
+ expect(storage.client_secret).to eq('cs')
41
+ expect(storage.dir_name ).to eq('dn')
42
+ expect(storage.path ).to eq 'myback'
43
+ expect(storage.max_retries ).to be(2)
44
+ expect(storage.retry_waitsec).to be(3)
45
+ end
46
+ end
47
+
48
+ describe '#transfer!' do
49
+ let(:timestamp) { Time.now.strftime("%Y.%m.%d.%H.%M.%S") }
50
+ let(:remote_path) { File.join('myback/test_trigger', timestamp) }
51
+ let(:package) { double }
52
+ let(:file) { double }
53
+ let(:client) { double }
54
+ let(:cached_session) { double }
55
+ let(:storage) {
56
+ Storage::PCS.new(model, 'sid') do |c|
57
+ c.keep = 1
58
+ c.client_id = 'ci'
59
+ c.client_secret = 'cs'
60
+ c.dir_name = 'dn'
61
+ c.path = 'myback'
62
+ c.max_retries = 2
63
+ c.retry_waitsec = 3
64
+ end
65
+ }
66
+
67
+ before do
68
+ allow(storage).to receive(:package).and_return(package)
69
+ allow(package).to receive(:trigger).and_return('test_trigger')
70
+ allow(package).to receive(:time).and_return(timestamp)
71
+ allow(file).to receive(:size).and_return(68*1024*1024)
72
+ allow(storage).to receive(:client).and_return(client)
73
+ allow(storage).to receive(:cached_session).and_return(cached_session)
74
+ end
75
+
76
+ it 'transfers files' do
77
+ allow(client).to receive(:upload)
78
+ allow(package).to receive(:filenames).
79
+ and_return(['test_trigger.tar_aa', 'test_trigger.tar_ab'])
80
+ src = File.join(Config.tmp_path, 'test_trigger.tar_aa')
81
+ dest = File.join(remote_path, 'test_trigger.tar_aa')
82
+ expect(File).to receive(:open).with(src, 'r').and_yield(file)
83
+ expect(client).to receive(:upload).with(file, path: dest, block_upload: true,
84
+ retry_times: 2,
85
+ retry_waitsec: 3)
86
+
87
+ src = File.join(Config.tmp_path, 'test_trigger.tar_ab')
88
+ dest = File.join(remote_path, 'test_trigger.tar_ab')
89
+ expect(File).to receive(:open).with(src, 'r').and_yield(file)
90
+ expect(client).to receive(:upload).with(file, path: dest, block_upload: true,
91
+ retry_times: 2,
92
+ retry_waitsec: 3)
93
+ expect(storage).to receive(:auto_refresh_token).and_yield.twice
94
+ storage.send(:transfer!)
95
+ end
96
+
97
+ describe 'when access token expired' do
98
+ let(:oauth_client) { double }
99
+ let(:session) { double }
100
+
101
+ before do
102
+ allow(package).to receive(:filenames).and_return(['test_trigger.tar_aa'])
103
+ allow(client).to receive(:upload).and_raise(Baidu::Errors::AuthError, 'expired')
104
+ allow(storage).to receive(:write_cache!).with(session) do
105
+ allow(client).to receive(:upload).and_return(nil) # do not raise like before
106
+ end
107
+ allow(storage).to receive(:oauth_client).and_return(oauth_client)
108
+ allow(cached_session).to receive(:refresh_token)
109
+ end
110
+
111
+ it 'refreshes access token automatically with valid refresh token' do
112
+ allow(oauth_client).to receive(:refresh).and_return(session)
113
+
114
+ src = File.join(Config.tmp_path, 'test_trigger.tar_aa')
115
+ dest = File.join(remote_path, 'test_trigger.tar_aa')
116
+ expect(Logger).to receive(:info).with("Storing '#{dest}'...")
117
+ expect(File).to receive(:open).with(src, 'r').and_yield(file).twice
118
+ expect(client).to receive(:upload).with(file, path: dest, block_upload: true,
119
+ retry_times: 2,
120
+ retry_waitsec: 3).twice
121
+ expect(Logger).to receive(:info).with('Refreshing Baidu session...')
122
+ storage.send(:transfer!)
123
+ end
124
+
125
+ it 'refreshes access token automatically with invalid refresh token' do
126
+ allow(cached_session).to receive(:refresh_token).and_return(nil)
127
+ allow(oauth_client).to receive(:refresh).and_return(nil)
128
+
129
+ src = File.join(Config.tmp_path, 'test_trigger.tar_aa')
130
+ dest = File.join(remote_path, 'test_trigger.tar_aa')
131
+ expect(Logger).to receive(:info).with("Storing '#{dest}'...")
132
+ expect(File).to receive(:open).with(src, 'r').and_yield(file).once
133
+ expect(Logger).to receive(:info).with('Refreshing Baidu session...')
134
+ expect(oauth_client).to receive(:refresh)
135
+ expect(Kernel).to receive(:raise)
136
+ expect(storage).to receive(:write_cache!).never
137
+ expect {
138
+ storage.send(:transfer!)
139
+ }.to raise_error(Storage::PCS::Error)
140
+ end
141
+ end
142
+ end
143
+
144
+ describe '#remove!' do
145
+ let(:timestamp) { Time.now.strftime("%Y.%m.%d.%H.%M.%S") }
146
+ let(:remote_path) { File.join('myback/test_trigger', timestamp) }
147
+ let(:package) { double }
148
+ let(:client) { double }
149
+ let(:storage) {
150
+ Storage::PCS.new(model, 'sid') do |c|
151
+ c.keep = 1
152
+ c.client_id = 'ci'
153
+ c.client_secret = 'cs'
154
+ c.dir_name = 'dn'
155
+ c.path = 'myback'
156
+ c.max_retries = 2
157
+ c.retry_waitsec = 3
158
+ end
159
+ }
160
+
161
+ before do
162
+ allow(storage).to receive(:package).and_return(package)
163
+ allow(package).to receive(:trigger).and_return('test_trigger')
164
+ allow(package).to receive(:time).and_return(timestamp)
165
+ allow(storage).to receive(:client).and_return(client)
166
+ end
167
+
168
+ it 'removes package' do
169
+ expect(Logger).to receive(:info).with("Removing backup package dated #{ timestamp }...")
170
+ expect(client).to receive(:delete).with(remote_path)
171
+
172
+ storage.send(:remove!, package)
173
+ end
174
+ end
175
+
176
+ describe '#client' do
177
+ let(:storage) {
178
+ Storage::PCS.new(model, 'sid') do |c|
179
+ c.keep = 1
180
+ c.client_id = 'ci'
181
+ c.client_secret = 'cs'
182
+ c.dir_name = 'dn'
183
+ c.path = 'myback'
184
+ c.max_retries = 2
185
+ c.retry_waitsec = 3
186
+ end
187
+ }
188
+
189
+ it 'creates new session' do
190
+ session = double
191
+ client = double
192
+ allow(storage).to receive(:cached_session).and_return(nil)
193
+ allow(storage).to receive(:create_session!).and_return(session)
194
+ allow(Baidu::PCS::Client).to receive(:new).with(session, 'dn').and_return(client)
195
+
196
+ expect(Logger).to receive(:info).with('Creating a new Baidu session!')
197
+ expect(storage).to receive(:create_session!).once.and_return(session)
198
+
199
+ storage.send(:client)
200
+ expect(storage.instance_variable_get(:@client)).to eq(client)
201
+ end
202
+
203
+ it 'uses cached session' do
204
+ session = double
205
+ client = double
206
+ allow(storage).to receive(:cached_session).and_return(session)
207
+ allow(Baidu::PCS::Client).to receive(:new).with(session, 'dn').and_return(client)
208
+
209
+ expect(storage).to receive(:create_session!).never
210
+
211
+ storage.send(:client)
212
+ expect(storage.instance_variable_get(:@client)).to eq(client)
213
+ end
214
+ end
215
+
216
+ describe '#cached_session' do
217
+ let(:storage) {
218
+ Storage::PCS.new(model, 'sid') do |c|
219
+ c.keep = 1
220
+ c.client_id = 'ci'
221
+ c.client_secret = 'cs'
222
+ c.dir_name = 'dn'
223
+ c.path = 'myback'
224
+ c.max_retries = 2
225
+ c.retry_waitsec = 3
226
+ end
227
+ }
228
+
229
+ it 'has no cached file' do
230
+ expect(File).to receive(:exist?).and_return(false)
231
+ expect(storage.send(:cached_session)).to eq(false)
232
+ end
233
+
234
+ it 'loads cached session' do
235
+ session = double
236
+ content = double
237
+ base64_c = double
238
+ cached_file = storage.send(:cached_file)
239
+ expect(File).to receive(:exist?).with(cached_file).and_return(true)
240
+ expect(File).to receive(:read).with(cached_file).and_return(content)
241
+ expect(Base64).to receive(:decode64).with(content).and_return(base64_c)
242
+ expect(Marshal).to receive(:load).with(base64_c).and_return(session)
243
+ expect(Logger).to receive(:info).with('Baidu session data loaded from cache!')
244
+
245
+ expect(storage.send(:cached_session)).to eq(session)
246
+ end
247
+
248
+ it 'loads cached session failed' do
249
+ expect(File).to receive(:exist?).and_return(true)
250
+ expect(File).to receive(:read).and_raise('error.')
251
+ expect(Logger).to receive(:warn)
252
+ expect(storage.send(:cached_session)).to eq(false)
253
+ end
254
+ end
255
+
256
+ describe '#auto_refresh_token' do
257
+ let(:session) { double }
258
+ let(:oauth_client) { double }
259
+ let(:cached_session) { double }
260
+ let(:storage) {
261
+ Storage::PCS.new(model, 'sid') do |c|
262
+ c.keep = 1
263
+ c.client_id = 'ci'
264
+ c.client_secret = 'cs'
265
+ c.dir_name = 'dn'
266
+ c.path = 'myback'
267
+ c.max_retries = 2
268
+ c.retry_waitsec = 3
269
+ end
270
+ }
271
+
272
+ before do
273
+ allow(storage).to receive(:oauth_client).and_return(oauth_client)
274
+ allow(storage).to receive(:cached_session).and_return(cached_session)
275
+ allow(cached_session).to receive(:refresh_token)
276
+ end
277
+
278
+ it 'does not raise autu error' do
279
+ expect(Logger).to receive(:info).with('Refreshing Baidu session...').never
280
+ expect(oauth_client).to receive(:refresh).never
281
+ expect(storage).to receive(:write_cache!).never
282
+
283
+ storage.send(:auto_refresh_token) { }
284
+ end
285
+
286
+ it 'raises auth error with valid refresh token' do
287
+ work_p = proc { raise Baidu::Errors::AuthError, 'expired' }
288
+ allow(oauth_client).to receive(:refresh).and_return(session)
289
+ allow(storage).to receive(:write_cache!) do
290
+ work_p = proc { }
291
+ end
292
+
293
+ expect(Logger).to receive(:info).with('Refreshing Baidu session...')
294
+ expect(oauth_client).to receive(:refresh)
295
+ expect(storage).to receive(:write_cache!).with(session)
296
+
297
+ storage.send(:auto_refresh_token) { work_p.call }
298
+ end
299
+
300
+ it 'raises auth error with invalid refresh token' do
301
+ work_p = proc { raise Baidu::Errors::AuthError, 'expired' }
302
+ allow(oauth_client).to receive(:refresh).and_return(nil)
303
+ allow(storage).to receive(:write_cache!) do
304
+ work_p = proc { }
305
+ end
306
+
307
+ expect(Logger).to receive(:info).with('Refreshing Baidu session...')
308
+ expect(oauth_client).to receive(:refresh)
309
+ expect(storage).to receive(:write_cache!).never
310
+
311
+ expect {
312
+ storage.send(:auto_refresh_token) { work_p.call }
313
+ }.to raise_error(Storage::PCS::Error)
314
+ end
315
+
316
+ it 'raises when there are too many auth errors' do
317
+ work_p = proc { raise Baidu::Errors::AuthError, 'expired' }
318
+ allow(oauth_client).to receive(:refresh).and_return(session)
319
+ expect {
320
+ storage.send(:auto_refresh_token) { work_p.call }
321
+ }.to raise_error(Storage::PCS::Error, 'Storage::PCS::Error: Too many auth errors')
322
+ end
323
+ end
324
+
325
+ describe '#cached_file' do
326
+ it 'has right cached file path' do
327
+ path1 = Storage::PCS.new(model, 'sid') do |c|
328
+ c.client_id = 'ci'
329
+ end.send(:cached_file)
330
+ expect(path1).to eq(Config.cache_path + '/pcs_sid_ci')
331
+
332
+ path2 = Storage::PCS.new(model) do |c|
333
+ c.client_id = 'ci2'
334
+ end.send(:cached_file)
335
+ expect(path2).to eq(Config.cache_path + '/pcs__ci2')
336
+ end
337
+ end
338
+
339
+ describe '#write_cache!' do
340
+ let(:cached_file) { double }
341
+ let(:session) { double }
342
+
343
+ before do
344
+ storage.client_id = 'ci'
345
+ allow(FileUtils).to receive(:mkdir_p)
346
+ allow(storage).to receive(:cached_file).and_return(cached_file)
347
+ end
348
+
349
+ it 'writes cached file' do
350
+ expect(FileUtils).to receive(:mkdir_p).with(Config.cache_path)
351
+ expect(File).to receive(:open).with(cached_file, 'w').and_yield(cached_file)
352
+ data = Base64.encode64(Marshal.dump(session))
353
+ expect(cached_file).to receive(:write).with(data)
354
+
355
+ storage.send(:write_cache!, session)
356
+ end
357
+ end
358
+
359
+ describe '#oauth' do
360
+ before do
361
+ storage.client_id = 'ci'
362
+ storage.client_secret = 'cs'
363
+ end
364
+
365
+ it 'creates oauth client' do
366
+ oclient = double
367
+ expect(Baidu::OAuth::Client).to receive(:new).with('ci', 'cs').and_return(oclient)
368
+ expect(storage.send(:oauth_client)).to eq(oclient)
369
+ end
370
+ end
371
+
372
+ describe '#create_session!' do
373
+ let(:rest) { Hash.new }
374
+ before do
375
+ storage.client_id = 'ci'
376
+ storage.client_secret = 'cs'
377
+ end
378
+
379
+ it 'is authorized successfully' do
380
+ device_flow = double
381
+ oauth_client = double
382
+ session = double
383
+ expect(storage).to receive(:oauth_client).and_return(oauth_client)
384
+ expect(oauth_client).to receive(:device_flow).and_return(device_flow)
385
+ expect(device_flow).to receive(:user_and_device_code).with('netdisk').and_return(rest)
386
+
387
+ rest[:verification_url] = 'https://example.com/verification_url'
388
+ rest[:user_code] = 'xxxyyy'
389
+ rest[:device_code] = 'zzzzzz'
390
+
391
+ expect(STDOUT).to receive(:puts).with('1. Visit verification url: https://example.com/verification_url')
392
+ expect(STDOUT).to receive(:puts).with('2. Type user code below in the form')
393
+ expect(STDOUT).to receive(:puts).with("\t xxxyyy")
394
+ expect(STDOUT).to receive(:puts).with("3. Hit 'Enter/Return' once you're authorized.")
395
+ expect(STDIN).to receive(:gets)
396
+ expect(STDOUT).to receive(:puts).with('Baidu session cached: ' + storage.send(:cached_file))
397
+
398
+ expect(device_flow).to receive(:get_token).with(rest[:device_code]).and_return(session)
399
+ expect(storage).to receive(:write_cache!).with(session)
400
+
401
+ expect(storage.send(:create_session!)).to eq(session)
402
+ end
403
+
404
+ it 'is not authorized' do
405
+ allow(storage).to receive(:oauth_client).and_raise('any error')
406
+ expect {
407
+ storage.send(:create_session!)
408
+ }.to raise_error(Storage::PCS::Error)
409
+ end
410
+ end
411
+ end
412
+ end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: backup-pcs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Lonre Wang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: baidu-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: backup
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.9.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.9.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ description: Backup Storage for supporting Baidu Personal Cloud Storage(PCS)
56
+ email:
57
+ - me@wanglong.me
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".travis.yml"
65
+ - Gemfile
66
+ - Guardfile
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - backup-pcs.gemspec
71
+ - lib/backup/pcs.rb
72
+ - lib/backup/pcs/version.rb
73
+ - lib/backup/storage/pcs.rb
74
+ - spec/pcs_spec.rb
75
+ - spec/spec_helper.rb
76
+ homepage: https://github.com/lonre/backup-pcs
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.1.11
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Baidu PCS Storage for Backup
100
+ test_files:
101
+ - spec/pcs_spec.rb
102
+ - spec/spec_helper.rb
103
+ has_rdoc: