backup-pcs 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/Gemfile +16 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +51 -0
- data/Rakefile +4 -0
- data/backup-pcs.gemspec +24 -0
- data/lib/backup/pcs/version.rb +5 -0
- data/lib/backup/pcs.rb +3 -0
- data/lib/backup/storage/pcs.rb +131 -0
- data/spec/pcs_spec.rb +412 -0
- data/spec/spec_helper.rb +17 -0
- metadata +103 -0
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
data/.rspec
ADDED
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
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 [](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
data/backup-pcs.gemspec
ADDED
@@ -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
|
data/lib/backup/pcs.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|