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.
@@ -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,9 @@
1
+ module QcloudCos
2
+ class Configuration
3
+ attr_accessor :app_id, :secret_id, :secret_key, :bucket, :endpoint, :ssl_ca_file, :max_retry_times
4
+
5
+ def max_retry_times
6
+ @max_retry_times || MAX_RETRY_TIMES
7
+ end
8
+ end
9
+ 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,9 @@
1
+ module QcloudCos
2
+ module Objectable
3
+ def initialize(hash)
4
+ hash.each do |k, v|
5
+ send("#{k}=", v)
6
+ end
7
+ end
8
+ end
9
+ 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