#!/usr/bin/env python # -*- coding:utf-8 -*- # project: 3月 # author: liuyu # date: 2020/3/22 # pip install aliyun-python-sdk-sts oss2 import hashlib import json import logging import os import re import time import oss2 from aliyunsdkcore import client from aliyunsdksts.request.v20150401 import AssumeRoleRequest from oss2 import SizedFileAdapter, determine_part_size from oss2.models import PartInfo from api.utils.modelutils import get_filename_form_file logger = logging.getLogger(__name__) # 以下代码展示了STS的用法,包括角色扮演获取临时用户的密钥、使用临时用户的密钥访问OSS。 # STS入门教程请参看 https://yq.aliyun.com/articles/57895 # STS的官方文档请参看 https://help.aliyun.com/document_detail/28627.html # 首先初始化AccessKeyId、AccessKeySecret、Endpoint等信息。 # 通过环境变量获取,或者把诸如“<你的AccessKeyId>”替换成真实的AccessKeyId等。 # 注意:AccessKeyId、AccessKeySecret为子用户的密钥。 # 子用户需要有 调用STS服务AssumeRole接口的权限 # 创建ram用户,授权 管理对象存储服务(OSS)权限 # RoleArn可以在控制台的“访问控制 > RAM角色管理 > 创建的ram用户 > 基本信息 > Arn”上查看。 # # 以杭州区域为例,Endpoint可以是: # http://oss-cn-hangzhou.aliyuncs.com # https://oss-cn-hangzhou.aliyuncs.com # 分别以HTTP、HTTPS协议访问。 def md5sum(src): m = hashlib.md5() m.update(src) return m.hexdigest() class AliYunCdn(object): def __init__(self, key, is_https, domain_name): uri = 'https://' if is_https else 'http://' self.domain = uri + domain_name self.key = key # 鉴权方式A def a_auth(self, uri, exp): p = re.compile("^(http://|https://)?([^/?]+)(/[^?]*)?(\\?.*)?$") if not p: return None m = p.match(uri) scheme, host, path, args = m.groups() if not scheme: scheme = "http://" if not path: path = "/" if not args: args = "" rand = "0" # "0" by default, other value is ok uid = "0" # "0" by default, other value is ok ss_string = f"{path}-{exp}-{rand}-{uid}-{self.key}" hash_value = md5sum(ss_string.encode("utf-8")) auth_key = f"{exp}-{rand}-{uid}-{hash_value}" if args: return f"{scheme}{host}{path}{args}&auth_key={auth_key}" else: return f"{scheme}{host}{path}{args}?auth_key={auth_key}" def get_cdn_download_token(self, filename, expires=1800): uri = f"{self.domain}/{filename}" exp = int(time.time()) + expires download_url = self.a_auth(uri, exp) logger.info(f"make cdn download url {download_url}") return download_url class StsToken(object): """AssumeRole返回的临时用户密钥 :param str access_key_id: 临时用户的access key id :param str access_key_secret: 临时用户的access key secret :param int expiration: 过期时间,UNIX时间,自1970年1月1日UTC零点的秒数 :param str security_token: 临时用户Token :param str request_id: 请求ID """ def __init__(self): self.access_key_id = '' self.access_key_secret = '' self.expiration = 3600 self.security_token = '' self.request_id = '' self.bucket = '' self.endpoint = '' class AliYunOss(object): def __init__(self, access_key, secret_key, bucket_name, endpoint, sts_role_arn, is_https, domain_name=None, download_auth_type=1, cnd_auth_key=None): self.access_key_id = access_key self.access_key_secret = secret_key self.bucket_name = bucket_name self.endpoint = endpoint self.sts_role_arn = sts_role_arn self.region_id = '-'.join(self.endpoint.split('.')[0].split("-")[1:3]) self.is_https = is_https self.download_auth_type = download_auth_type self.cnd_auth_key = cnd_auth_key self.domain_name = domain_name self.bucket_get = None self.bucket = None self.make_auth_bucket('init_get', 1800, True) self.make_auth_bucket('init_auth', 1800) def fetch_sts_token(self, name, expires, only_put=False, only_get=False): """子用户角色扮演获取临时用户的密钥 :param only_put: 是否只允许上传 :param only_get: 是否只允许下载 :param expires: 过期时间 :param name: obj name :return StsToken: 临时用户密钥 """ clt = client.AcsClient(self.access_key_id, self.access_key_secret, self.region_id) req = AssumeRoleRequest.AssumeRoleRequest() req.set_accept_format('json') req.set_RoleArn(self.sts_role_arn) req.set_RoleSessionName(name) req.set_DurationSeconds(expires) p_action = "oss:GetObject" if only_put: p_action = ["oss:PutObject", "oss:AbortMultipartUpload"] if only_get: p_action = "oss:GetObject" name = '*' if only_get or only_put: policy = { "Statement": [ { "Action": p_action, "Effect": "Allow", "Resource": [f"acs:oss:*:*:{self.bucket_name}/{name}"] } ], "Version": "1" } req.set_Policy(json.dumps(policy)) body = clt.do_action_with_exception(req) j = json.loads(oss2.to_unicode(body)) token = StsToken() token.access_key_id = j['Credentials']['AccessKeyId'] token.access_key_secret = j['Credentials']['AccessKeySecret'] token.security_token = j['Credentials']['SecurityToken'] token.request_id = j['RequestId'] token.expiration = oss2.utils.to_unixtime(j['Credentials']['Expiration'], '%Y-%m-%dT%H:%M:%SZ') token.bucket = self.bucket_name token.endpoint = self.endpoint.replace('-internal', '') logger.info(f"get aliyun sts token {token.__dict__}") return token def get_upload_token(self, name, expires=1800): return self.fetch_sts_token(name, expires, only_put=True).__dict__ def make_auth_bucket(self, name, expires, only_get=False): uri = 'https://' if self.is_https else 'http://' url = self.endpoint is_cname = False if self.domain_name and self.download_auth_type == 1: url = self.domain_name is_cname = True token = self.fetch_sts_token(name, expires, only_get=only_get) auth = oss2.StsAuth(token.access_key_id, token.access_key_secret, token.security_token) if only_get: self.bucket_get = oss2.Bucket(auth, uri + url, self.bucket_name, is_cname=is_cname) else: self.bucket = oss2.Bucket(auth, uri + url, self.bucket_name, is_cname=is_cname) def get_download_url(self, name, expires=1800, force_new=False): private_url = self.bucket_get.sign_url('GET', name, expires) return private_url def del_file(self, name): # self.fetch_sts_token(name,expires=300) # auth = oss2.StsAuth(self.token.access_key_id, self.token.access_key_secret, self.token.security_token) # bucket = oss2.Bucket(auth, self.endpoint,self.bucket_name) return self.bucket.delete_object(name) def rename_file(self, old_filename, new_filename): self.bucket.copy_object(self.bucket_name, old_filename, new_filename) return self.del_file(old_filename) def upload_file(self, local_file_full_path): return self.multipart_upload_file(local_file_full_path) if os.path.isfile(local_file_full_path): filename = os.path.basename(local_file_full_path) headers = { 'Content-Disposition': f'attachment; filename="{filename}"', 'Cache-Control': '' } self.bucket.put_object_from_file(filename, local_file_full_path, headers) # with open(local_file_full_path, 'rb') as fileobj: # # Seek方法用于指定从第1000个字节位置开始读写。上传时会从您指定的第1000个字节位置开始上传,直到文件结束。 # fileobj.seek(1000, os.SEEK_SET) # # Tell方法用于返回当前位置。 # # current = fileobj.tell() # self.bucket.put_object(os.path.basename(local_file_full_path), fileobj) return True else: logger.error(f"file {local_file_full_path} is not file") def download_file(self, name, local_file_full_path): dir_path = os.path.dirname(local_file_full_path) if not os.path.exists(dir_path): os.makedirs(dir_path) return self.bucket.get_object_to_file(name, local_file_full_path) def get_file_info(self, name): result = self.bucket.head_object(name) base_info = {} if result.content_length: base_info['content_length'] = result.content_length if result.last_modified: base_info['last_modified'] = result.last_modified return base_info def multipart_upload_file(self, local_file_full_path): if os.path.isfile(local_file_full_path): total_size = os.path.getsize(local_file_full_path) # determine_part_size方法用于确定分片大小。 part_size = determine_part_size(total_size, preferred_size=1024 * 1024 * 10) filename = os.path.basename(local_file_full_path) headers = { 'Content-Disposition': 'attachment; filename="%s"' % get_filename_form_file(filename).encode( "utf-8").decode("latin1"), 'Cache-Control': '' } # 初始化分片。 # 如需在初始化分片时设置文件存储类型,请在init_multipart_upload中设置相关headers,参考如下。 # headers = dict() # headers["x-oss-storage-class"] = "Standard" upload_id = self.bucket.init_multipart_upload(filename, headers=headers).upload_id parts = [] # 逐个上传分片。 with open(local_file_full_path, 'rb') as f: part_number = 1 offset = 0 while offset < total_size: num_to_upload = min(part_size, total_size - offset) # 调用SizedFileAdapter(fileobj, size)方法会生成一个新的文件对象,重新计算起始追加位置。 result = self.bucket.upload_part(filename, upload_id, part_number, SizedFileAdapter(f, num_to_upload)) parts.append(PartInfo(part_number, result.etag)) offset += num_to_upload part_number += 1 self.bucket.complete_multipart_upload(filename, upload_id, parts) return True else: logger.error(f"file {local_file_full_path} is not file")