qcloud_cos 0.1.0 → 0.3.0
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 +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
|