backup-pcs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![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
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:
|