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.
@@ -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