qcloud_cos 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +37 -0
- data/.travis.yml +4 -1
- data/Gemfile +1 -0
- data/README.md +79 -46
- data/Rakefile +15 -5
- data/bin/console +12 -3
- data/lib/qcloud_cos.rb +63 -2
- data/lib/qcloud_cos/api.rb +365 -0
- data/lib/qcloud_cos/authorization.rb +65 -0
- data/lib/qcloud_cos/configuration.rb +9 -0
- data/lib/qcloud_cos/convenient_api.rb +107 -0
- data/lib/qcloud_cos/error.rb +49 -0
- data/lib/qcloud_cos/http.rb +81 -0
- data/lib/qcloud_cos/model/file_object.rb +16 -0
- data/lib/qcloud_cos/model/folder_object.rb +35 -0
- data/lib/qcloud_cos/model/list.rb +34 -0
- data/lib/qcloud_cos/model/objectable.rb +9 -0
- data/lib/qcloud_cos/utils.rb +47 -0
- data/lib/qcloud_cos/version.rb +1 -1
- data/qcloud_cos.gemspec +19 -11
- data/wiki/get_started.md +761 -0
- metadata +122 -11
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'openssl'
|
5
|
+
require 'digest'
|
6
|
+
|
7
|
+
module QcloudCos
|
8
|
+
class Authorization
|
9
|
+
attr_reader :config
|
10
|
+
|
11
|
+
# 用于对请求进行签名
|
12
|
+
# @param config [Configration] specify configuration for sign
|
13
|
+
#
|
14
|
+
def initialize(config)
|
15
|
+
@config = config
|
16
|
+
end
|
17
|
+
|
18
|
+
# 生成单次有效签名
|
19
|
+
#
|
20
|
+
# @param bucket [String] 指定 Bucket 名字
|
21
|
+
# @param fileid [String] 指定要签名的资源
|
22
|
+
def sign_once(bucket, fileid)
|
23
|
+
sign_base(bucket, fileid, 0)
|
24
|
+
end
|
25
|
+
|
26
|
+
# 生成多次有效签名
|
27
|
+
#
|
28
|
+
# @param bucket [String] 指定 Bucket 名字
|
29
|
+
# @param expired [Integer] (EXPIRED_SECONDS) 指定签名过期时间, 秒作为单位
|
30
|
+
def sign_more(bucket, expired = EXPIRED_SECONDS)
|
31
|
+
sign_base(bucket, nil, current_time + expired)
|
32
|
+
end
|
33
|
+
alias_method :sign, :sign_more
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def sign_base(bucket, fileid, expired)
|
38
|
+
fileid = "/#{app_id}#{fileid}" if fileid
|
39
|
+
|
40
|
+
src_str = "a=#{app_id}&b=#{bucket}&k=#{secret_id}&e=#{expired}&t=#{current_time}&r=#{rdm}&f=#{fileid}"
|
41
|
+
|
42
|
+
Base64.encode64("#{OpenSSL::HMAC.digest('sha1', secret_key, src_str)}#{src_str}").delete("\n").strip
|
43
|
+
end
|
44
|
+
|
45
|
+
def app_id
|
46
|
+
config.app_id
|
47
|
+
end
|
48
|
+
|
49
|
+
def secret_id
|
50
|
+
config.secret_id
|
51
|
+
end
|
52
|
+
|
53
|
+
def secret_key
|
54
|
+
config.secret_key
|
55
|
+
end
|
56
|
+
|
57
|
+
def current_time
|
58
|
+
Time.now.to_i
|
59
|
+
end
|
60
|
+
|
61
|
+
def rdm
|
62
|
+
rand(10**9)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module QcloudCos
|
2
|
+
module ConvenientApi
|
3
|
+
|
4
|
+
# 获取 Bucket 信息
|
5
|
+
#
|
6
|
+
# @param bucket_name [String] :bucket (config.bucket) 指定当前 bucket, 默认是配置里面的 bucket
|
7
|
+
#
|
8
|
+
# @return [Hash] 返回 Bucket 信息
|
9
|
+
def bucket_info(bucket_name = config.bucket)
|
10
|
+
stat('/', bucket: bucket_name)['data']
|
11
|
+
rescue
|
12
|
+
{}
|
13
|
+
end
|
14
|
+
|
15
|
+
# 返回该路径下文件和文件夹的数目
|
16
|
+
#
|
17
|
+
# @param path [String] 指定路径
|
18
|
+
# @param options [Hash] 额外参数
|
19
|
+
# @option options [String] :bucket (config.bucket) 指定当前 bucket, 默认是配置里面的 bucket
|
20
|
+
#
|
21
|
+
# @return [Hash]
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# QcloudCos.count('/path/to/folder/') #=> { folder_count: 100, file_count: 1000 }
|
25
|
+
def count(path = '/', options = {})
|
26
|
+
result = list_folders(path, options.merge(num: 1))
|
27
|
+
{
|
28
|
+
folder_count: result.dircount || 0,
|
29
|
+
file_count: result.filecount || 0
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# 判断该路径下是否为空
|
34
|
+
#
|
35
|
+
# @param path [String] 指定路径
|
36
|
+
# @param options [Hash] 额外参数
|
37
|
+
# @option options [String] :bucket (config.bucket) 指定当前 bucket, 默认是配置里面的 bucket
|
38
|
+
#
|
39
|
+
# @return [Boolean]
|
40
|
+
def empty?(path = '/', options = {})
|
41
|
+
count(path, options).values.uniq == 0
|
42
|
+
end
|
43
|
+
|
44
|
+
# 判断该路径下是否有文件
|
45
|
+
#
|
46
|
+
# @param path [String] 指定路径
|
47
|
+
# @param options [Hash] 额外参数
|
48
|
+
# @option options [String] :bucket (config.bucket) 指定当前 bucket, 默认是配置里面的 bucket
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
def contains_file?(path = '/', options = {})
|
52
|
+
!count(path, options)[:file_count].zero?
|
53
|
+
end
|
54
|
+
|
55
|
+
# 判断该路径下是否有文件夹
|
56
|
+
#
|
57
|
+
# @param path [String] 指定路径
|
58
|
+
# @param options [Hash] 额外参数
|
59
|
+
# @option options [String] :bucket (config.bucket) 指定当前 bucket, 默认是配置里面的 bucket
|
60
|
+
#
|
61
|
+
# @return [Boolean]
|
62
|
+
def contains_folder?(path = '/', options = {})
|
63
|
+
!count(path, options)[:folder_count].zero?
|
64
|
+
end
|
65
|
+
|
66
|
+
# 判断文件或者文件夹是否存在
|
67
|
+
#
|
68
|
+
# @param path [String] 指定文件路径
|
69
|
+
# @param options [Hash] 额外参数
|
70
|
+
# @option options [String] :bucket (config.bucket) 指定当前 bucket, 默认是配置里面的 bucket
|
71
|
+
#
|
72
|
+
# @return [Boolean]
|
73
|
+
def exists?(path = '/', options = {})
|
74
|
+
return true if path == '/' || path.to_s.empty?
|
75
|
+
result = stat(path, options)
|
76
|
+
result.key?('data') && result['data'].key?('name')
|
77
|
+
rescue
|
78
|
+
false
|
79
|
+
end
|
80
|
+
alias_method :exist?, :exists?
|
81
|
+
|
82
|
+
# 获取文件外网访问地址
|
83
|
+
#
|
84
|
+
# @param path [String] 指定文件路径
|
85
|
+
# @param options [Hash] 额外参数
|
86
|
+
# @option options [String] :bucket (config.bucket_name) 指定当前 bucket, 默认是配置里面的 bucket
|
87
|
+
# @option options [Integer] :expired (600) 指定有效期, 秒为单位
|
88
|
+
#
|
89
|
+
# @raise [FileNotExistError] 如果文件不存在
|
90
|
+
# @raise [InvalidFilePathError] 如果文件路径不合法
|
91
|
+
#
|
92
|
+
# @return [String] 下载地址
|
93
|
+
def public_url(path, options = {})
|
94
|
+
path = fixed_path(path)
|
95
|
+
bucket = validates(path, options)
|
96
|
+
|
97
|
+
result = stat(path, options)
|
98
|
+
if result.key?('data') && result['data'].key?('access_url')
|
99
|
+
expired = options['expired'] || PUBLIC_EXPIRED_SECONDS
|
100
|
+
sign = authorization.sign(bucket, expired)
|
101
|
+
"#{result['data']['access_url']}?sign=#{sign}"
|
102
|
+
else
|
103
|
+
fail FileNotExistError
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module QcloudCos
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
class RequestError < Error
|
6
|
+
attr_reader :code
|
7
|
+
attr_reader :message
|
8
|
+
attr_reader :origin_response
|
9
|
+
|
10
|
+
def initialize(response)
|
11
|
+
if response.parsed_response.key?('code')
|
12
|
+
@code = response.parsed_response['code']
|
13
|
+
@message = response.parsed_response['message']
|
14
|
+
end
|
15
|
+
@origin_response = response
|
16
|
+
super("ERROR Code: #{@code}, Message: #{@message}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class InvalidFolderPathError < Error
|
21
|
+
def initialize(msg)
|
22
|
+
super(msg)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class InvalidFilePathError < Error
|
27
|
+
def initialize
|
28
|
+
super('文件名不能以 / 结尾')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class FileNotExistError < Error
|
33
|
+
def initialize
|
34
|
+
super('文件不存在')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class MissingBucketError < Error
|
39
|
+
def initialize
|
40
|
+
super('缺少 Bucket 参数或者 Bucket 不存在')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class MissingSessionIdError < Error
|
45
|
+
def initialize
|
46
|
+
super('分片上传不能缺少 Session ID')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'httmultiparty'
|
3
|
+
require 'addressable/uri'
|
4
|
+
require 'qcloud_cos/error'
|
5
|
+
|
6
|
+
module QcloudCos
|
7
|
+
class Http
|
8
|
+
include HTTParty
|
9
|
+
include HTTMultiParty
|
10
|
+
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(url, options = {})
|
18
|
+
request('GET', url, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def post(url, options = {})
|
22
|
+
request('POST', url, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def request(verb, url, options = {})
|
28
|
+
query = options.fetch(:query, {})
|
29
|
+
headers = options.fetch(:headers, {})
|
30
|
+
body = options.delete(:body)
|
31
|
+
|
32
|
+
append_headers!(headers, verb, body, options)
|
33
|
+
options = { headers: headers, query: query, body: body }
|
34
|
+
append_options!(options, url)
|
35
|
+
|
36
|
+
wrap(self.class.__send__(verb.downcase, url, options))
|
37
|
+
end
|
38
|
+
|
39
|
+
def wrap(response)
|
40
|
+
if response.code == 200 && response.parsed_response['code'] == 0
|
41
|
+
response
|
42
|
+
else
|
43
|
+
fail RequestError, response
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def append_headers!(headers, _verb, body, _options)
|
48
|
+
append_default_headers!(headers)
|
49
|
+
append_body_headers!(headers, body)
|
50
|
+
end
|
51
|
+
|
52
|
+
def append_options!(options, url)
|
53
|
+
options.merge!(uri_adapter: Addressable::URI)
|
54
|
+
if config.ssl_ca_file
|
55
|
+
options.merge!(ssl_ca_file: config.ssl_ca_file)
|
56
|
+
elsif url.start_with?('https://')
|
57
|
+
options.merge!(verify_peer: true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def append_default_headers!(headers)
|
62
|
+
headers.merge!(default_headers)
|
63
|
+
end
|
64
|
+
|
65
|
+
def append_body_headers!(headers, body)
|
66
|
+
headers.merge!('Content-Length' => Utils.content_size(body).to_s) if body
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_headers
|
70
|
+
{
|
71
|
+
'User-Agent' => user_agent,
|
72
|
+
'Host' => 'web.file.myqcloud.com'
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def user_agent
|
77
|
+
"qcloud-cos-sdk-ruby/#{QcloudCos::VERSION} " \
|
78
|
+
"(#{RbConfig::CONFIG['host_os']} ruby-#{RbConfig::CONFIG['ruby_version']})"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'qcloud_cos/model/objectable'
|
2
|
+
|
3
|
+
module QcloudCos
|
4
|
+
class FileObject
|
5
|
+
include Objectable
|
6
|
+
attr_accessor :access_url
|
7
|
+
attr_accessor :source_url
|
8
|
+
attr_accessor :biz_attr
|
9
|
+
attr_accessor :ctime
|
10
|
+
attr_accessor :filelen
|
11
|
+
attr_accessor :filesize
|
12
|
+
attr_accessor :mtime
|
13
|
+
attr_accessor :name
|
14
|
+
attr_accessor :sha
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'qcloud_cos/model/objectable'
|
3
|
+
|
4
|
+
module QcloudCos
|
5
|
+
class FolderObject
|
6
|
+
include Objectable
|
7
|
+
MAXLENGTH = 20
|
8
|
+
RETAINED_SYMBOLS = %w(/ ? * : | \ < > ")
|
9
|
+
RETAINED_FIELDS = %w(con aux nul prn com0 com1 com2 com3 com4 com5 com6 com7 com8 com9 lpt0 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9)
|
10
|
+
|
11
|
+
attr_accessor :biz_attr
|
12
|
+
attr_accessor :ctime
|
13
|
+
attr_accessor :mtime
|
14
|
+
attr_accessor :name
|
15
|
+
|
16
|
+
# 校验文件夹路径
|
17
|
+
#
|
18
|
+
# @param path [String] 文件夹路径
|
19
|
+
#
|
20
|
+
# @raise InvalidFolderPathError 如果文件夹路径不合法
|
21
|
+
def self.validate(path)
|
22
|
+
if !path.end_with?('/')
|
23
|
+
fail InvalidFolderPathError, '文件夹路径必须以 / 结尾'
|
24
|
+
elsif !(names = path.split('/')).empty?
|
25
|
+
if names.detect { |name| RETAINED_FIELDS.include?(name.downcase) }
|
26
|
+
fail InvalidFolderPathError, %(文件夹名字不能是保留字段: '#{RETAINED_FIELDS.join("', '")}')
|
27
|
+
elsif names.detect { |name| name.match(/[\/?*:|\\<>"]/) }
|
28
|
+
fail InvalidFolderPathError, %(文件夹名字不能包含保留字符: '#{RETAINED_SYMBOLS.join("', '")}')
|
29
|
+
elsif names.detect { |name| name.length > MAXLENGTH }
|
30
|
+
fail InvalidFolderPathError, %(文件夹名字不能超过#{MAXLENGTH}个字符)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'qcloud_cos/model/file_object'
|
2
|
+
require 'qcloud_cos/model/folder_object'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module QcloudCos
|
6
|
+
class List
|
7
|
+
include Enumerable
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :context, :dircount, :filecount, :has_more
|
11
|
+
def_delegators :@objects, :[], :each, :size, :inspect
|
12
|
+
|
13
|
+
# 自动将 Hash 构建成对象
|
14
|
+
def initialize(result)
|
15
|
+
@context = result['context']
|
16
|
+
@dircount = result['dircount']
|
17
|
+
@filecount = result['filecount']
|
18
|
+
@has_more = result['has_more']
|
19
|
+
@objects = build_objects(result['infos'])
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def build_objects(objects)
|
25
|
+
objects.map do |obj|
|
26
|
+
if obj.key?('access_url')
|
27
|
+
QcloudCos::FileObject.new(obj)
|
28
|
+
else
|
29
|
+
QcloudCos::FolderObject.new(obj)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module QcloudCos
|
2
|
+
class Utils
|
3
|
+
class << self
|
4
|
+
# 对 path 进行 url_encode
|
5
|
+
def url_encode(path)
|
6
|
+
ERB::Util.url_encode(path).gsub('%2F', '/')
|
7
|
+
end
|
8
|
+
|
9
|
+
# 计算 content 的大小
|
10
|
+
def content_size(content)
|
11
|
+
if content.respond_to?(:size)
|
12
|
+
content.size
|
13
|
+
elsif content.is_a?(IO)
|
14
|
+
content.stat.size
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# 生成 content 的 sha
|
19
|
+
def generate_sha(content)
|
20
|
+
Digest::SHA1.hexdigest content
|
21
|
+
end
|
22
|
+
|
23
|
+
# 生成文件的 sha1 值
|
24
|
+
def generate_file_sha(file_path)
|
25
|
+
Digest::SHA1.file(file_path).hexdigest
|
26
|
+
end
|
27
|
+
|
28
|
+
# 将 hash 的 key 统一转化为 string
|
29
|
+
def stringify_keys!(hash)
|
30
|
+
hash.keys.each do |key|
|
31
|
+
hash[key.to_s] = hash.delete(key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @example
|
36
|
+
#
|
37
|
+
# Utils.hash_slice({ 'a' => 1, 'b' => 2, 'c' => 3 }, 'a', 'c') # { 'a' => 1, 'c' => 3 }
|
38
|
+
#
|
39
|
+
# 获取 Hash 中的一部分键值对
|
40
|
+
def hash_slice(hash, *selected_keys)
|
41
|
+
new_hash = {}
|
42
|
+
selected_keys.each { |k| new_hash[k] = hash[k] if hash.key?(k) }
|
43
|
+
new_hash
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|