lib/aliyun/connection.rb
require 'openssl'
require 'digest/md5'
require 'rest-client'
require 'base64'
require 'uri'
require 'aliyun/data_center'
module Aliyun
class Connection
include DataCenter
# The upload host according to the connection configurations
attr_reader :aliyun_upload_host
# The internal host
attr_reader :aliyun_internal_host
# The external host
attr_reader :aliyun_external_host
# The alias host
attr_reader :aliyun_alias_host
attr_reader :aliyun_protocol
attr_reader :aliyun_protocol_relative_url
# Initialize the OSS connection
#
# @param [Hash] An options to specify connection details
# @option access_id [String] used to set "Authorization" request header
# @option access_key [String] the access key
# @option bucket [String] bucket used to access
# @option data_center [String] available data center name, e.g. 'cn-hangzhou'
# @option internal [true, false] if the service should be accessed through internal network
# @option host_alias [String] the alias of the host, such as the CDN domain name
# @option protocol [String] 'http' or 'https', default to 'http'
# @option protocol_relative_url [true, false] if to use protocol relative url, https://en.wikipedia.org/wiki/Wikipedia:Protocol-relative_URL
# @note both access_id and acces_key are related to authorization algorithm:
# https://docs.aliyun.com/#/pub/oss/api-reference/access-control&signature-header
def initialize(options = {})
@aliyun_access_id = options[:access_id]
@aliyun_access_key = options[:access_key]
@aliyun_bucket = options[:bucket]
@aliyun_protocol_relative_url = !!options[:protocol_relative_url]
@aliyun_protocol = options[:protocol] || 'http'
@aliyun_upload_host = "#{@aliyun_bucket}.#{get_endpoint(options)}"
@aliyun_internal_host = "#{@aliyun_bucket}.#{get_endpoint(options.merge(internal: true))}"
@aliyun_external_host = "#{@aliyun_bucket}.#{get_endpoint(options.merge(internal: false))}"
@aliyun_alias_host = options[:host_alias] || @aliyun_upload_host
end
# Return the meta informations for a file specified by the path
# https://docs.aliyun.com/#/pub/oss/api-reference/object&HeadObject
#
# @param path [String] the path of file storaged in Aliyun OSS
# @return [Hash] the meta data of the file
# @note the example headers will be like:
#
# {
# {:date=>"Sun, 02 Aug 2015 02:42:45 GMT",
# :content_type=>"image/jpg",
# :content_length=>"125198",
# :connection=>"close",
# :accept_ranges=>"bytes",
# :etag=>"\"336262A42E5B99AFF5B8BC66611FC156\"",
# :last_modified=>"Sun, 01 Dec 2013 16:39:57 GMT",
# :server=>"AliyunOSS",
# :x_oss_object_type=>"Normal",
# :x_oss_request_id=>"55BD83A5D4C05BDFF4A329E0"}}
#
def head(path)
path = format_path(path)
bucket_path = get_bucket_path(path)
date = gmtdate
headers = {
'Host' => @aliyun_upload_host,
'Date' => date,
'Authorization' => sign('HEAD', bucket_path, '', '', date)
}
url = path_to_url(path)
RestClient.head(url, headers).headers
rescue RestClient::ResourceNotFound
{}
end
# Upload File to Aliyun OSS
# https://docs.aliyun.com/#/pub/oss/api-reference/object&PutObject
#
# @param path [String] the target storing path on the oss
# @param file [File] an instance of File represents a file to be uploaded
# @param options [Hash]
# - content_type - MimeType value for the file, default is "image/jpg"
#
# @return [String] The downloadable url of the uploaded file
# @return [nil] if the uploading failed
def put(path, file, options = {})
path = format_path(path)
bucket_path = get_bucket_path(path)
content_md5 = Digest::MD5.file(file).base64digest
content_type = options[:content_type] || 'image/jpg'
date = gmtdate
url = path_to_url(path)
auth_sign = sign('PUT', bucket_path, content_md5, content_type, date)
headers = {
'Authorization' => auth_sign,
'Content-Md5' => content_md5,
'Content-Type' => content_type,
'Content-Length' => file.size,
'Date' => date,
'Host' => @aliyun_upload_host,
'Expect' => '100-Continue'
}
response = RestClient.put(url, file, headers)
response.code == 200 ? path_to_url(path) : nil
end
# Delete a file from the OSS
# https://docs.aliyun.com/#/pub/oss/api-reference/object&DeleteObject
#
# @param path [String] the path to retrieve the file on remote storage
# @return [String] the expired url to the file, if the file deleted successfully
# @return [nil] if the delete operation failed
def delete(path)
path = format_path(path)
bucket_path = get_bucket_path(path)
date = gmtdate
headers = {
'Host' => @aliyun_upload_host,
'Date' => date,
'Authorization' => sign('DELETE', bucket_path, '', '', date)
}
url = path_to_url(path)
response = RestClient.delete(url, headers)
response.code == 204 ? url : nil
end
# Download the file from OSS
# https://docs.aliyun.com/#/pub/oss/api-reference/object&GetObject
#
# @param path [String] the path to retrieve the file on remote storage
# @return [?] the file content consist of bytes
def get(path)
path = format_path(path)
bucket_path = get_bucket_path(path)
date = gmtdate
headers = {
'Host' => @aliyun_upload_host,
'Date' => date,
'Authorization' => sign('GET', bucket_path, '', '', date)
}
url = path_to_url(path)
response = RestClient.get(url, headers)
response.body
end
# Determine if the file exists on the OSS
# https://docs.aliyun.com/#/pub/oss/api-reference/object&HeadObject
#
# @param path [String] the path to retrieve the file on remote storage
# @return [true] if file exists
# @return [false] if file could not be found
def exists?(path)
head(path).empty? ? false : true
end
# The GMT format time referenced from HTTP 1.1
# https://docs.aliyun.com/#/pub/oss/api-reference/public-header
#
# @return [String] a string represents the formated time, e.g. "Wed, 05 Sep. 2012 23:00:00 GMT"
def gmtdate
Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT')
end
# remove leading slashes in the path
#
# @param path [String] the path to retrieve the file on remote storage
# @return [String] the new string after removing leading slashed
def format_path(path)
path.blank? ? '' : path.gsub(%r{^/+}, '')
end
# A path consis of the bucket name and file name
# https://docs.aliyun.com/#/pub/oss/api-reference/access-control&signature-header
#
# @param path [String] the path to retrieve the file on remote storage
# @return [String] the expected bucket path, e.g. "test-bucket/oss-api.pdf"
def get_bucket_path(path)
[@aliyun_bucket, path].join('/')
end
# The full path contains host name to the file
#
# @param path [String] the path to retrieve the file on remote storage
# @return [String] the expected full path, e.g. "http://martin-test.oss-cn-hangzhou.aliyuncs.com/oss-api.pdf"
def path_to_url(path)
URI.encode(path =~ %r{^https?://} ? path : "http://#{aliyun_upload_host}/#{path}")
end
private
# The signature algorithm
# https://docs.aliyun.com/#/pub/oss/api-reference/access-control&signature-header
#
# @param verb [String] the request verb, e.g. "GET" or "DELETE"
# @param content_md5 [String] the md5 value for the content to be uploaded
# @param content_type [String] the content type of the file, e.g. "application/pdf"
# @param date [String] the GMT formatted date string
def sign(verb, path, content_md5, content_type, date)
canonicalized_oss_headers = ''
canonicalized_resource = "/#{path}"
string_to_sign = [
verb, content_md5, content_type, date,
canonicalized_oss_headers + canonicalized_resource
].join("\n")
digest = OpenSSL::Digest.new('sha1')
h = OpenSSL::HMAC.digest(digest, @aliyun_access_key, string_to_sign)
"OSS #{@aliyun_access_id}:#{Base64.encode64(h)}"
end
end
end