cos 0.1.0 → 0.1.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 +4 -4
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -2
- data/Gemfile +4 -1
- data/LICENSE +191 -0
- data/README.md +2014 -17
- data/Rakefile +23 -6
- data/bin/cos +325 -0
- data/bin/setup +1 -3
- data/cos.gemspec +24 -13
- data/lib/cos.rb +41 -4
- data/lib/cos/api.rb +289 -0
- data/lib/cos/bucket.rb +731 -0
- data/lib/cos/checkpoint.rb +62 -0
- data/lib/cos/client.rb +58 -0
- data/lib/cos/config.rb +102 -0
- data/lib/cos/dir.rb +301 -0
- data/lib/cos/download.rb +252 -0
- data/lib/cos/exception.rb +62 -0
- data/lib/cos/file.rb +152 -0
- data/lib/cos/http.rb +95 -0
- data/lib/cos/logging.rb +47 -0
- data/lib/cos/resource.rb +201 -0
- data/lib/cos/signature.rb +119 -0
- data/lib/cos/slice.rb +292 -0
- data/lib/cos/struct.rb +49 -0
- data/lib/cos/tree.rb +165 -0
- data/lib/cos/util.rb +82 -0
- data/lib/cos/version.rb +2 -2
- data/spec/cos/bucket_spec.rb +562 -0
- data/spec/cos/client_spec.rb +77 -0
- data/spec/cos/dir_spec.rb +195 -0
- data/spec/cos/download_spec.rb +105 -0
- data/spec/cos/http_spec.rb +70 -0
- data/spec/cos/signature_spec.rb +83 -0
- data/spec/cos/slice_spec.rb +302 -0
- data/spec/cos/struct_spec.rb +38 -0
- data/spec/cos/tree_spec.rb +322 -0
- data/spec/cos/util_spec.rb +106 -0
- data/test/download_test.rb +44 -0
- data/test/list_test.rb +43 -0
- data/test/upload_test.rb +48 -0
- metadata +132 -21
- data/.idea/.name +0 -1
- data/.idea/cos.iml +0 -49
- data/.idea/encodings.xml +0 -6
- data/.idea/misc.xml +0 -14
- data/.idea/modules.xml +0 -8
- data/.idea/workspace.xml +0 -465
- data/bin/console +0 -14
data/lib/cos/http.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'rest-client'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module COS
|
7
|
+
|
8
|
+
class HTTP
|
9
|
+
|
10
|
+
# 默认content-type
|
11
|
+
DEFAULT_CONTENT_TYPE = 'application/json'
|
12
|
+
|
13
|
+
# 请求创建超时
|
14
|
+
OPEN_TIMEOUT = 15
|
15
|
+
|
16
|
+
# 响应读取超时
|
17
|
+
READ_TIMEOUT = 120
|
18
|
+
|
19
|
+
include Logging
|
20
|
+
|
21
|
+
attr_reader :config, :signature
|
22
|
+
|
23
|
+
def initialize(config)
|
24
|
+
@config = config
|
25
|
+
@signature = Signature.new(config)
|
26
|
+
end
|
27
|
+
|
28
|
+
# GET请求
|
29
|
+
def get(path, headers, signature = nil)
|
30
|
+
do_request('GET', path, headers, signature)
|
31
|
+
end
|
32
|
+
|
33
|
+
# POST请求
|
34
|
+
def post(path, headers, signature, payload)
|
35
|
+
do_request('POST', path, headers, signature, payload)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# 发送请求
|
41
|
+
def do_request(method, path, headers, signature = nil, payload = nil)
|
42
|
+
|
43
|
+
# 整理头部信息
|
44
|
+
headers['content-type'] ||= DEFAULT_CONTENT_TYPE
|
45
|
+
headers['user-agent'] = get_user_agent
|
46
|
+
headers['authorization'] = signature
|
47
|
+
headers['accept'] = 'application/json'
|
48
|
+
|
49
|
+
# 请求地址
|
50
|
+
url = "#{config.api_base}#{path}"
|
51
|
+
|
52
|
+
logger.debug("Send HTTP request, method: #{method}, url: " \
|
53
|
+
"#{url}, headers: #{headers}, payload: #{payload}")
|
54
|
+
|
55
|
+
response = RestClient::Request.execute(
|
56
|
+
:method => method,
|
57
|
+
:url => URI.encode(url),
|
58
|
+
:headers => headers,
|
59
|
+
:payload => payload,
|
60
|
+
:open_timeout => @config.open_timeout || OPEN_TIMEOUT,
|
61
|
+
:timeout => @config.read_timeout || READ_TIMEOUT
|
62
|
+
) do |resp, request, result, &blk|
|
63
|
+
|
64
|
+
# 捕获异常
|
65
|
+
if resp.code >= 300
|
66
|
+
e = ServerError.new(resp)
|
67
|
+
logger.warn(e.to_s)
|
68
|
+
raise e
|
69
|
+
else
|
70
|
+
resp.return!(request, result, &blk)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
logger.debug("Received HTTP response, code: #{response.code}, headers: " \
|
76
|
+
"#{response.headers}, body: #{response.body}")
|
77
|
+
|
78
|
+
# 解析JSON结果
|
79
|
+
parse_data(response)
|
80
|
+
end
|
81
|
+
|
82
|
+
# 解析结果json 取出data部分
|
83
|
+
def parse_data(response)
|
84
|
+
j = JSON.parse(response.body, symbolize_names: true)
|
85
|
+
j[:data]
|
86
|
+
end
|
87
|
+
|
88
|
+
# 获取user agent
|
89
|
+
def get_user_agent
|
90
|
+
"cos-ruby-sdk/#{VERSION} ruby-#{RUBY_VERSION}/#{RUBY_PLATFORM}"
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
data/lib/cos/logging.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module COS
|
6
|
+
|
7
|
+
module Logging
|
8
|
+
|
9
|
+
# 默认日志存储
|
10
|
+
DEFAULT_LOG_FILE = './cos-sdk.log'
|
11
|
+
# 日志最大数量
|
12
|
+
MAX_NUM_LOG = 100
|
13
|
+
# 日志覆盖大小
|
14
|
+
ROTATE_SIZE = 10 * 1024 * 1024
|
15
|
+
|
16
|
+
# 设置日志输出的文件
|
17
|
+
# level = Logger::DEBUG | Logger::INFO | Logger::ERROR | Logger::FATAL
|
18
|
+
def self.set_logger(file, level)
|
19
|
+
if file == STDOUT or file == STDERR
|
20
|
+
@logger = Logger.new(file)
|
21
|
+
@logger.level = level
|
22
|
+
else
|
23
|
+
@logger = Logger.new(file, MAX_NUM_LOG, ROTATE_SIZE)
|
24
|
+
@logger.level = level
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# 获取logger
|
29
|
+
def logger
|
30
|
+
Logging.logger
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# 实例方法使用logger
|
36
|
+
def self.logger
|
37
|
+
unless @logger
|
38
|
+
@logger = Logger.new(DEFAULT_LOG_FILE, MAX_NUM_LOG, ROTATE_SIZE)
|
39
|
+
@logger.level = Logger::INFO
|
40
|
+
end
|
41
|
+
|
42
|
+
@logger
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/lib/cos/resource.rb
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module COS
|
4
|
+
|
5
|
+
class Resource
|
6
|
+
|
7
|
+
attr_reader :bucket, :path, :dir_count, :file_count
|
8
|
+
|
9
|
+
# 实例化COS资源迭代器
|
10
|
+
def initialize(bucket, path, options = {})
|
11
|
+
@bucket = bucket
|
12
|
+
@path = path
|
13
|
+
@more = options
|
14
|
+
@results = Array.new
|
15
|
+
@dir_count = 0
|
16
|
+
@file_count = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
# 实现迭代器
|
20
|
+
def next
|
21
|
+
loop do
|
22
|
+
# 从接口获取下一页结果
|
23
|
+
fetch_more if @results.empty?
|
24
|
+
|
25
|
+
# 取出结果
|
26
|
+
r = @results.shift
|
27
|
+
break unless r
|
28
|
+
|
29
|
+
yield r
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# 返回迭代器
|
34
|
+
def to_enum
|
35
|
+
self.enum_for(:next)
|
36
|
+
end
|
37
|
+
|
38
|
+
# 获取列表
|
39
|
+
def fetch
|
40
|
+
client = bucket.client
|
41
|
+
resp = client.api.list(path, @more.merge({bucket: bucket.bucket_name}))
|
42
|
+
|
43
|
+
# 遍历结果转换为对应的对象
|
44
|
+
@results = resp[:infos].map do |r|
|
45
|
+
if r[:filesize].nil?
|
46
|
+
# 目录
|
47
|
+
COSDir.new(r.merge({
|
48
|
+
bucket: bucket,
|
49
|
+
path: Util.get_list_path(path, r[:name])
|
50
|
+
}))
|
51
|
+
else
|
52
|
+
# 文件
|
53
|
+
COSFile.new(r.merge({
|
54
|
+
bucket: bucket,
|
55
|
+
path: Util.get_list_path(path, r[:name], true)
|
56
|
+
}))
|
57
|
+
end
|
58
|
+
end || []
|
59
|
+
|
60
|
+
@dir_count = resp[:dir_count]
|
61
|
+
@file_count = resp[:file_count]
|
62
|
+
|
63
|
+
@more[:context] = resp[:context]
|
64
|
+
@more[:has_more] = resp[:has_more]
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# 如果有更多页继续获取下一页
|
70
|
+
def fetch_more
|
71
|
+
return if @more[:has_more] == false
|
72
|
+
fetch
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# COS资源,文件与目录的共有操作
|
78
|
+
class ResourceOperator < Struct::Base
|
79
|
+
|
80
|
+
required_attrs :bucket, :path, :name, :ctime, :mtime
|
81
|
+
optional_attrs :biz_attr, :filesize, :filelen, :sha, :access_url,
|
82
|
+
# 根目录(bucket)参数
|
83
|
+
:authority, :bucket_type, :migrate_source_domain,
|
84
|
+
:need_preview, :refers, :blackrefers, :brower_exec,
|
85
|
+
:cnames, :nugc_flag, :SKIP_EXTRA
|
86
|
+
|
87
|
+
# 资源类型
|
88
|
+
attr_reader :type
|
89
|
+
|
90
|
+
alias :file_size :filesize
|
91
|
+
alias :file_len :filelen
|
92
|
+
|
93
|
+
def initialize(attrs)
|
94
|
+
super(attrs)
|
95
|
+
end
|
96
|
+
|
97
|
+
# 创建时间Time类型
|
98
|
+
#
|
99
|
+
# @return [Time]
|
100
|
+
def created_at
|
101
|
+
Time.at(ctime.to_i)
|
102
|
+
end
|
103
|
+
|
104
|
+
# 更新时间Time类型
|
105
|
+
#
|
106
|
+
# @return [Time]
|
107
|
+
def updated_at
|
108
|
+
Time.at(mtime.to_i)
|
109
|
+
end
|
110
|
+
|
111
|
+
# 参数转化为Hash类型
|
112
|
+
#
|
113
|
+
# @return [Hash]
|
114
|
+
def to_hash
|
115
|
+
hash = {
|
116
|
+
type: type,
|
117
|
+
bucket: bucket.bucket_name,
|
118
|
+
path: path,
|
119
|
+
name: name,
|
120
|
+
ctime: ctime,
|
121
|
+
mtime: mtime,
|
122
|
+
}
|
123
|
+
|
124
|
+
optional_attrs.each do |key|
|
125
|
+
hash[key] = send(key.to_s) if respond_to?(key) and send(key.to_s) != nil
|
126
|
+
end
|
127
|
+
|
128
|
+
hash
|
129
|
+
end
|
130
|
+
|
131
|
+
# 判断当前资源是否存在
|
132
|
+
#
|
133
|
+
# @raise [ServerError] 服务端异常返回
|
134
|
+
#
|
135
|
+
# @return [Boolean] 是否存在
|
136
|
+
#
|
137
|
+
# @example
|
138
|
+
# puts resource.exist?
|
139
|
+
def exist?
|
140
|
+
bucket.exist?(path)
|
141
|
+
end
|
142
|
+
|
143
|
+
alias :exists? :exist?
|
144
|
+
|
145
|
+
# 获取(刷新)当前资源的状态
|
146
|
+
#
|
147
|
+
# @note 如查询根目录('/', '')可以获取到bucket信息, 返回COSDir
|
148
|
+
#
|
149
|
+
# @raise [ServerError] 服务端异常返回
|
150
|
+
#
|
151
|
+
# @return [COSFile|COSDir] 如果是目录则返回COSDir资源对象,是文件则返回COSFile资源对象
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
# puts resource.stat.name
|
155
|
+
def stat
|
156
|
+
bucket.stat(path)
|
157
|
+
end
|
158
|
+
|
159
|
+
# 更新当前资源的属性
|
160
|
+
#
|
161
|
+
# @note 根目录('/') 不可更新, 否则会抛出异常
|
162
|
+
#
|
163
|
+
# @param biz_attr [String] 目录/文件属性,业务端维护
|
164
|
+
#
|
165
|
+
# @raise [ServerError] 服务端异常返回
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# resource.update('i am the attr')
|
169
|
+
def update(biz_attr)
|
170
|
+
bucket.update(path, biz_attr)
|
171
|
+
@mtime = Time.now.to_i.to_s
|
172
|
+
@biz_attr = biz_attr
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
# 删除当前资源
|
177
|
+
#
|
178
|
+
# @note 非空目录及根目录不可删除,会抛出异常
|
179
|
+
#
|
180
|
+
# @raise [ServerError] 服务端异常返回
|
181
|
+
#
|
182
|
+
# @example
|
183
|
+
# resource.delete
|
184
|
+
def delete
|
185
|
+
bucket.delete(path)
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
# 删除当前资源, 不会抛出异常而是返回布尔值
|
190
|
+
#
|
191
|
+
# @note 非空目录及根目录不可删除, 返回false
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# puts resource.delete!
|
195
|
+
def delete!
|
196
|
+
bucket.delete!(path)
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'base64'
|
5
|
+
require 'openssl'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module COS
|
9
|
+
|
10
|
+
class Signature
|
11
|
+
|
12
|
+
attr_reader :config, :expire_seconds, :file_id
|
13
|
+
|
14
|
+
# 初始化
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# COS::Signature.new(config)
|
18
|
+
#
|
19
|
+
# @param config [COS::Config] 客户端设置
|
20
|
+
#
|
21
|
+
# @see COS::Config
|
22
|
+
def initialize(config)
|
23
|
+
@config = config
|
24
|
+
end
|
25
|
+
|
26
|
+
# 单次有效签名
|
27
|
+
#
|
28
|
+
# @see http://www.qcloud.com/wiki/%E9%89%B4%E6%9D%83%E6%8A%80%E6%9C%AF%E6%9C%8D%E5%8A%A1%E6%96%B9%E6%A1%88#1_.E7.AD.BE.E5.90.8D.E4.B8.8E.E9.89.B4.E6.9D.83
|
29
|
+
#
|
30
|
+
# @note 用于删除目录、文件, 更新目录、文件
|
31
|
+
#
|
32
|
+
# @param bucket [String] bucket名称
|
33
|
+
# @param path [String] 文件或目录路径, 如 /path/, /path/file
|
34
|
+
#
|
35
|
+
# @return [String] 签名字符串
|
36
|
+
def once(bucket, path)
|
37
|
+
# 单次签名,需要指定资源存储的唯一标识
|
38
|
+
unless path.start_with?('/')
|
39
|
+
path = "/#{path}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# 资源存储的唯一标识,格式为"/app_id/bucket/用户自定义路径或资源名"
|
43
|
+
@file_id = "/#{config.app_id}/#{bucket}#{path}"
|
44
|
+
|
45
|
+
sign(:once, bucket)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# 多次有效签名
|
50
|
+
#
|
51
|
+
# @see http://www.qcloud.com/wiki/%E9%89%B4%E6%9D%83%E6%8A%80%E6%9C%AF%E6%9C%8D%E5%8A%A1%E6%96%B9%E6%A1%88#1_.E7.AD.BE.E5.90.8D.E4.B8.8E.E9.89.B4.E6.9D.83
|
52
|
+
#
|
53
|
+
# @note 用于上传,查询目录、文件,查询目录、文件,创建目录,下载(开启token防盗链)
|
54
|
+
#
|
55
|
+
# @param bucket [String] bucket名称
|
56
|
+
# @param expire_seconds [Integer] 签名有效时间(单位秒)
|
57
|
+
#
|
58
|
+
# @return [String] 签名字符串
|
59
|
+
def multiple(bucket, expire_seconds = config.multiple_sign_expire)
|
60
|
+
# 多次签名时,过期时间应大于当前时间
|
61
|
+
if expire_seconds <= 0
|
62
|
+
raise AttrError, 'Multiple signature expire_seconds must greater than 0'
|
63
|
+
end
|
64
|
+
|
65
|
+
@expire_seconds = expire_seconds
|
66
|
+
|
67
|
+
sign(:multiple, bucket)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# 生成签名
|
73
|
+
#
|
74
|
+
# @see http://www.qcloud.com/wiki/%E9%89%B4%E6%9D%83%E6%8A%80%E6%9C%AF%E6%9C%8D%E5%8A%A1%E6%96%B9%E6%A1%88#2.3.09.E7.94.9F.E6.88.90.E7.AD.BE.E5.90.8D
|
75
|
+
def sign(sign_type, bucket)
|
76
|
+
# 准备待签名字符串
|
77
|
+
sign_string = string_to_sign(sign_type, bucket)
|
78
|
+
|
79
|
+
# HMAC-SHA1算法进行签名
|
80
|
+
hmac_sha1 = OpenSSL::HMAC.digest('sha1', config.secret_key, sign_string)
|
81
|
+
|
82
|
+
# 然后将原字符串附加到签名结果的末尾,再进行Base64编码
|
83
|
+
Base64.strict_encode64("#{hmac_sha1}#{sign_string}")
|
84
|
+
end
|
85
|
+
|
86
|
+
# 准备签名字符串
|
87
|
+
#
|
88
|
+
# @see http://www.qcloud.com/wiki/%E9%89%B4%E6%9D%83%E6%8A%80%E6%9C%AF%E6%9C%8D%E5%8A%A1%E6%96%B9%E6%A1%88#2_.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95
|
89
|
+
def string_to_sign(sign_type, bucket)
|
90
|
+
# 随机串,无符号10进制整数,最长10位
|
91
|
+
r = rand(99999)
|
92
|
+
|
93
|
+
# 当前时间戳,是一个符合UNIX Epoch时间戳规范的数值,单位为秒
|
94
|
+
t = Time.now.to_i
|
95
|
+
|
96
|
+
if sign_type == :once
|
97
|
+
# 单次签名时e为0
|
98
|
+
e = 0
|
99
|
+
|
100
|
+
# 需要对其中非'/'字符进行urlencode编码
|
101
|
+
f = URI.encode(file_id)
|
102
|
+
|
103
|
+
elsif sign_type == :multiple
|
104
|
+
e = t + expire_seconds
|
105
|
+
|
106
|
+
# 多次签名不需要提供file_id
|
107
|
+
f = nil
|
108
|
+
|
109
|
+
else
|
110
|
+
raise Exception, 'Unknown sign type when prepare sign string'
|
111
|
+
end
|
112
|
+
|
113
|
+
# 拼接待签名字符串
|
114
|
+
"a=#{config.app_id}&b=#{bucket}&k=#{config.secret_id}&e=#{e}&t=#{t}&r=#{r}&f=#{f}"
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|