diff --git a/fir_client/src/components/FirAppInfostimeline.vue b/fir_client/src/components/FirAppInfostimeline.vue index 143c6a3..f21b914 100644 --- a/fir_client/src/components/FirAppInfostimeline.vue +++ b/fir_client/src/components/FirAppInfostimeline.vue @@ -182,7 +182,14 @@ this.$confirm('确认删除 ' + this.currentapp.name + '下 当前 release 版本吗?') // eslint-disable-next-line no-unused-vars .then(res => { + let loadingobj = this.$loading({ + lock: true, + text: '操作中,请耐心等待', + spinner: 'el-icon-loading', + // background: 'rgba(0, 0, 0, 0.7)' + }); releaseapputils(data => { + loadingobj.close(); if (data.code === 1000) { this.$message({ message: '删除成功', diff --git a/fir_client/src/components/FirUserProfileStorage.vue b/fir_client/src/components/FirUserProfileStorage.vue index c90d6d9..05bcc54 100644 --- a/fir_client/src/components/FirUserProfileStorage.vue +++ b/fir_client/src/components/FirUserProfileStorage.vue @@ -43,6 +43,23 @@ v-model="editstorageinfo.additionalparameter.endpoint"> + + + + + + + + + + + @@ -124,11 +141,26 @@ v-model="storageinfo.additionalparameter.endpoint"> - - + + + + + + + + + + + + @@ -226,6 +258,10 @@ activeName: 'chostorage', storage_info_lists: [], is_admin_storage: false, + download_auth_type_list: [{id: 1, name: 'OSS模式: 需要把OSS权限开启私有模式'}, { + id: 2, + name: 'CDN模式: 请先配置好阿里云CDN,开启阿里云OSS私有Bucket回源,将使用鉴权A方式' + },] } }, methods: { @@ -245,7 +281,7 @@ this.title = '新增存储'; this.disabled = false; this.dialogstorageVisible = true; - this.editstorageinfo = {'additionalparameter': {}}; + this.editstorageinfo = {'additionalparameter': {download_auth_type: 1}}; this.isaddflag = true; }, // eslint-disable-next-line no-unused-vars diff --git a/fir_ser/api/utils/app/supersignutils.py b/fir_ser/api/utils/app/supersignutils.py index b41b594..4578f16 100644 --- a/fir_ser/api/utils/app/supersignutils.py +++ b/fir_ser/api/utils/app/supersignutils.py @@ -11,10 +11,9 @@ from api.models import APPSuperSignUsedInfo, AppUDID, AppIOSDeveloperInfo, AppRe UDIDsyncDeveloper, DeveloperAppID, DeveloperDevicesID from api.utils.app.randomstrings import make_app_uuid, make_from_user_uuid from api.utils.serializer import get_developer_udided -from api.utils.storage.localApi import LocalStorage from api.utils.storage.caches import del_cache_response_by_short, send_msg_over_limit from api.utils.utils import file_format_path, delete_app_to_dev_and_file, delete_app_profile_file, \ - send_ios_developer_active_status, get_profile_full_path, delete_local_files + send_ios_developer_active_status, get_profile_full_path, delete_local_files, download_files_form_oss from api.utils.storage.storage import Storage logger = logging.getLogger(__file__) @@ -28,20 +27,7 @@ def check_org_file(user_obj, org_file): return storage_obj = Storage(user_obj) - download_url = storage_obj.get_download_url(os.path.basename(org_file), 600, key='check_org_file', force_new=True) - req = requests.get(download_url) - logger.info("download user %s file %s success" % (user_obj, org_file)) - - with open(org_file + ".check.tmp", "wb") as f: - f.write(req.content) - try: - if os.path.isfile(org_file): - os.remove(org_file) - os.rename(os.path.join(org_file + ".check.tmp"), org_file) - return True - except Exception as e: - logger.error("check org file and mv file %s failed Exception %s" % (org_file, e)) - return False + return download_files_form_oss(storage_obj, org_file) def resign_by_app_obj(app_obj, need_download_profile=True): diff --git a/fir_ser/api/utils/storage/aliyunApi.py b/fir_ser/api/utils/storage/aliyunApi.py index 337d70d..75fab9c 100644 --- a/fir_ser/api/utils/storage/aliyunApi.py +++ b/fir_ser/api/utils/storage/aliyunApi.py @@ -8,11 +8,15 @@ import json import os - from aliyunsdkcore import client from aliyunsdksts.request.v20150401 import AssumeRoleRequest - import oss2 +import re +import hashlib +import time +import logging + +logger = logging.getLogger(__file__) # 以下代码展示了STS的用法,包括角色扮演获取临时用户的密钥、使用临时用户的密钥访问OSS。 @@ -32,6 +36,46 @@ import oss2 # 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 = 'http://' + if is_https: + uri = 'https://' + 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 + sstring = "%s-%s-%s-%s-%s" % (path, exp, rand, uid, self.key) + hashvalue = md5sum(sstring.encode("utf-8")) + auth_key = "%s-%s-%s-%s" % (exp, rand, uid, hashvalue) + if args: + return "%s%s%s%s&auth_key=%s" % (scheme, host, path, args, auth_key) + else: + return "%s%s%s%s?auth_key=%s" % (scheme, host, path, args, auth_key) + + def get_cdn_download_token(self, filename, expires=1800): + uri = "%s/%s" % (self.domain, filename) + exp = int(time.time()) + expires + return self.a_auth(uri, exp) + + class StsToken(object): """AssumeRole返回的临时用户密钥 :param str access_key_id: 临时用户的access key id @@ -53,7 +97,8 @@ class StsToken(object): class AliYunOss(object): - def __init__(self, access_key, secret_key, bucket_name, endpoint, sts_role_arn, is_https, domain_name=None): + 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 @@ -62,6 +107,8 @@ class AliYunOss(object): self.region_id = '-'.join(self.endpoint.split('.')[0].split("-")[1:3]) self.token = StsToken() 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.get_auth_bucket('init', 900) @@ -91,6 +138,7 @@ class AliYunOss(object): self.token.expiration = oss2.utils.to_unixtime(j['Credentials']['Expiration'], '%Y-%m-%dT%H:%M:%SZ') self.token.bucket = self.bucket_name self.token.endpoint = self.endpoint + logger.info("get aliyun sts token %s" % self.token.__dict__) return self.token.__dict__ def get_upload_token(self, name, expires=1800): @@ -103,7 +151,7 @@ class AliYunOss(object): uri = 'https://' url = self.endpoint is_cname = False - if self.domain_name: + if self.domain_name and self.download_auth_type == 1: url = self.domain_name is_cname = True diff --git a/fir_ser/api/utils/storage/storage.py b/fir_ser/api/utils/storage/storage.py index 0daba5d..164b8db 100644 --- a/fir_ser/api/utils/storage/storage.py +++ b/fir_ser/api/utils/storage/storage.py @@ -5,7 +5,7 @@ # date: 2020/3/23 from api.models import UserInfo -from .aliyunApi import AliYunOss +from .aliyunApi import AliYunOss, AliYunCdn from .qiniuApi import QiNiuOss from .localApi import LocalStorage import json, time, base64 @@ -17,9 +17,9 @@ logger = logging.getLogger(__file__) class Storage(object): - def __init__(self, user, storage_obj=None): + def __init__(self, user, storage_obj=None, use_default_storage=False): try: - self.storage = self.get_storage(user, storage_obj) + self.storage = self.get_storage(user, storage_obj, use_default_storage) except Exception as e: logger.error("get %s storage failed Exception:%s" % (user, e)) self.storage = None @@ -36,8 +36,11 @@ class Storage(object): if download_val and not force_new: if download_val.get("time") > now - 60: return download_val.get("download_url") - - download_url = self.storage.get_download_url(filename, expires, force_new=True) + if self.storage.__dict__.get("download_auth_type", None) == 2: + cdn_obj = AliYunCdn(self.storage.cnd_auth_key, self.storage.is_https, self.storage.domain_name) + download_url = cdn_obj.get_cdn_download_token(filename, expires) + else: + download_url = self.storage.get_download_url(filename, expires, force_new=True) cache.set(down_key, {"download_url": download_url, "time": now + expires}, expires) return download_url @@ -67,11 +70,14 @@ class Storage(object): except Exception as e: logger.error("oss upload %s failed Exception %s" % (local_file_full_path, e)) - def get_storage(self, user, storage_obj): + def get_storage(self, user, storage_obj, use_default_storage): if storage_obj: self.storage_obj = storage_obj else: self.storage_obj = user.storage + if use_default_storage: + self.storage_obj = None + if self.storage_obj: auth = self.get_storage_auth(self.storage_obj) storage_key = "_".join([CACHE_KEY_TEMPLATE.get('user_storage_key'), user.uid, @@ -79,6 +85,7 @@ class Storage(object): storage_type = self.storage_obj.storage_type new_storage_obj = cache.get(storage_key) if new_storage_obj and not storage_obj: + logger.info("user %s get storage obj cache %s" % (user, new_storage_obj)) return new_storage_obj else: if storage_type == 1: @@ -87,17 +94,20 @@ class Storage(object): new_storage_obj = AliYunOss(**auth) else: new_storage_obj = LocalStorage(**auth) - + logger.info("user %s make storage obj %s" % (user, new_storage_obj)) new_storage_obj.storage_type = storage_type cache.set(storage_key, new_storage_obj, 600) return new_storage_obj else: - return self.get_default_storage(user, storage_obj) + logger.info("user %s has not storage obj, so get default" % user) + return self.get_default_storage(user, storage_obj, False) - def get_default_storage(self, user, storage_obj): + def get_default_storage(self, user, storage_obj, use_default_storage): admin_obj = UserInfo.objects.filter(is_superuser=True).order_by('pk').first() if admin_obj and admin_obj.storage: - return self.get_storage(admin_obj, storage_obj) + logger.info("user %s has not storage obj, from admin " + "get default storage" % user) + return self.get_storage(admin_obj, storage_obj, use_default_storage) else: storage_lists = THIRD_PART_CONFIG.get('storage') for storage in storage_lists: @@ -120,6 +130,8 @@ class Storage(object): new_storage_obj = LocalStorage(**auth) new_storage_obj.storage_type = 3 cache.set(storage_key, new_storage_obj, 600) + logger.info("user %s has not storage obj, admin already has not storage obj, from settings " + "get default storage %s" % (user, new_storage_obj)) return new_storage_obj return None diff --git a/fir_ser/api/utils/utils.py b/fir_ser/api/utils/utils.py index 3ca18ae..3b61f80 100644 --- a/fir_ser/api/utils/utils.py +++ b/fir_ser/api/utils/utils.py @@ -3,10 +3,10 @@ # project: 5月 # author: liuyu # date: 2020/5/7 -import os, re +import os, re, json, requests from fir_ser.settings import SUPER_SIGN_ROOT, SERVER_DOMAIN, CAPTCHA_LENGTH, MEDIA_ROOT from api.models import APPSuperSignUsedInfo, APPToDeveloper, \ - UDIDsyncDeveloper, UserInfo + UDIDsyncDeveloper, UserInfo, AppReleaseInfo from api.utils.app.randomstrings import make_app_uuid from api.utils.storage.localApi import LocalStorage from api.utils.storage.storage import Storage @@ -206,14 +206,98 @@ def send_ios_developer_active_status(user_info, msg): logger.warning("user %s has no email. so %s can't send!" % (user_info, msg)) +def get_filename_from_apptype(filename, apptype): + if apptype == 0: + filename = filename + '.apk' + else: + filename = filename + '.ipa' + return filename + + def delete_local_files(filename, apptype=None): storage = LocalStorage("localhost", False) if apptype is not None: - if apptype == 0: - filename = filename + '.apk' - else: - filename = filename + '.ipa' + filename = get_filename_from_apptype(filename, apptype) try: return storage.del_file(filename) except Exception as e: logger.error("delete file %s failed Exception %s" % (filename, e)) + + +def check_storage_additionalparameter(request, res): + data = request.data + try: + extra_parameters = data.get('additionalparameter', '') + if extra_parameters: + if not extra_parameters.get("download_auth_type", None): + extra_parameters['download_auth_type'] = 1 + if extra_parameters.get("download_auth_type", None) == 2: + if not extra_parameters.get("cnd_auth_key", None): + logger.error("user %s add new storage failed" % (request.user)) + res.msg = "cdn 鉴权KEY 缺失" + res.code = 1006 + return False, res + data['additionalparameters'] = json.dumps(extra_parameters) + except Exception as e: + logger.error("user:%s additionalparameters %s dumps failed Exception:%s" % ( + request.user, data.get('additionalparameter', ''), e)) + return True, res + + +def change_storage_and_change_head_img(user_obj, new_storage_obj): + migrating_storage_file_data(user_obj, user_obj.head_img, new_storage_obj) + + +def download_files_form_oss(storage_obj, org_file): + download_url = storage_obj.get_download_url(os.path.basename(org_file), 600, key='check_org_file', force_new=True) + req = requests.get(download_url) + if req.status_code == 200: + logger.info("download file %s success" % org_file) + else: + logger.error("download file %s failed %s" % (org_file, req.content)) + return False + + with open(org_file + ".check.tmp", "wb") as f: + f.write(req.content) + try: + if os.path.isfile(org_file): + os.remove(org_file) + os.rename(os.path.join(org_file + ".check.tmp"), org_file) + return True + except Exception as e: + logger.error("check org file and mv file %s failed Exception %s" % (org_file, e)) + return False + + +def migrating_storage_file_data(user_obj, filename, new_storage_obj): + local_file_full_path = os.path.join(MEDIA_ROOT, filename) + old_storage_obj = Storage(user_obj) + if not new_storage_obj: + new_storage_obj = Storage(user_obj, None, True) + else: + new_storage_obj = Storage(user_obj, new_storage_obj) + + if old_storage_obj.get_storage_type() == 3: + if new_storage_obj.get_storage_type() == 3: + pass + else: + # 本地向云存储上传,并删除本地数据 + new_storage_obj.upload_file(local_file_full_path) + delete_local_files(filename) + else: + if new_storage_obj.get_storage_type() == 3: + # 云存储下载 本地,并删除云存储 + if download_files_form_oss(old_storage_obj, local_file_full_path): + old_storage_obj.delete_file(filename) + else: + # 云存储互传,先下载本地,然后上传新云存储,删除本地和老云存储 + if download_files_form_oss(old_storage_obj, local_file_full_path): + new_storage_obj.upload_file(local_file_full_path) + delete_local_files(filename) + old_storage_obj.delete_file(filename) + + +def migrating_storage_data(user_obj, new_storage_obj): + for app_release_obj in AppReleaseInfo.objects.filter(app_id__user_id=user_obj).all(): + filename = get_filename_from_apptype(app_release_obj.release_id, app_release_obj.release_type) + migrating_storage_file_data(user_obj, filename, new_storage_obj) diff --git a/fir_ser/api/views/storage.py b/fir_ser/api/views/storage.py index 0f0103c..4f133ee 100644 --- a/fir_ser/api/views/storage.py +++ b/fir_ser/api/views/storage.py @@ -8,10 +8,10 @@ from rest_framework.views import APIView from api.utils.response import BaseResponse from api.utils.auth import ExpiringTokenAuthentication, StoragePermission from rest_framework.response import Response -import json from api.utils.storage.caches import del_cache_storage from api.models import AppStorage, UserInfo -from api.utils.utils import upload_oss_default_head_img +from api.utils.utils import upload_oss_default_head_img, check_storage_additionalparameter, \ + change_storage_and_change_head_img from api.utils.serializer import StorageSerializer import logging @@ -59,12 +59,9 @@ class StorageView(APIView): res = BaseResponse() data = request.data logger.info("user %s add new storage data:%s" % (request.user, data)) - try: - data['additionalparameters'] = json.dumps(data.get('additionalparameter', '')) - except Exception as e: - logger.error("user:%s additionalparameters %s dumps failed Exception:%s" % ( - request.user, data.get('additionalparameter', ''), e)) - + status, msg = check_storage_additionalparameter(request, res) + if not status: + return Response(msg.dict) serializer = StorageSerializer(data=data, context={'user_obj': request.user}) if serializer.is_valid(): storage_obj = serializer.save() @@ -96,10 +93,17 @@ class StorageView(APIView): if use_storage_id: try: if use_storage_id == -1: + change_storage_and_change_head_img(request.user, None) UserInfo.objects.filter(pk=request.user.pk).update(storage=None) + else: + new_storage_obj = AppStorage.objects.filter(pk=use_storage_id).first() + change_storage_and_change_head_img(request.user, new_storage_obj) UserInfo.objects.filter(pk=request.user.pk).update(storage_id=use_storage_id) + del_cache_storage(request.user) + # change_storage_and_change_head_img(request.user, old_storage_obj, new_storage_obj) + except Exception as e: logger.error("update user %s storage failed Exception:%s" % (request.user, e)) res.code = 1006 @@ -112,13 +116,9 @@ class StorageView(APIView): res.msg = '存储正在使用中,无法修改' res.code = 1007 return Response(res.dict) - try: - data['additionalparameters'] = json.dumps(data.get('additionalparameter', '')) - except Exception as e: - del data['additionalparameters'] - logger.error("user:%s additionalparameters %s dumps failed Exception:%s" % ( - request.user, data.get('additionalparameter', ''), e)) - + status, msg = check_storage_additionalparameter(request, res) + if not status: + return Response(msg.dict) storage_obj = AppStorage.objects.filter(id=storage_id, user_id=request.user).first() storage_obj_bak = AppStorage.objects.filter(id=storage_id, user_id=request.user).first() serializer = StorageSerializer(instance=storage_obj, data=data, context={'user_obj': request.user}, diff --git a/fir_ser/fir_ser/settings.py b/fir_ser/fir_ser/settings.py index 1c35b77..ad1caa5 100644 --- a/fir_ser/fir_ser/settings.py +++ b/fir_ser/fir_ser/settings.py @@ -211,7 +211,8 @@ THIRD_PART_CONFIG = { 'sts_role_arn': 'ap-sage', 'endpoint': 'oss-cn-beijing.aliyuncs.com', 'is_https': True, - 'domain_name': 'aoud.xin' + 'domain_name': 'aoud.xin', + 'download_auth_type': 1 # 1:oss 2:cdn }, 'active': False },