diff --git a/fir_client/src/components/FirLogin.vue b/fir_client/src/components/FirLogin.vue index c23ad93..97a9f03 100644 --- a/fir_client/src/components/FirLogin.vue +++ b/fir_client/src/components/FirLogin.vue @@ -116,7 +116,7 @@ }, cptch: {"cptch_image": '', "cptch_key": '', "length": 8}, activeName: 'username', - allow_ways: {'third':{}}, + allow_ways: {'third': {}}, rutitle: '', rctitle: '', register_enable: false, diff --git a/fir_client/src/components/apps/FirApps.vue b/fir_client/src/components/apps/FirApps.vue index ce7f609..d1fe605 100644 --- a/fir_client/src/components/apps/FirApps.vue +++ b/fir_client/src/components/apps/FirApps.vue @@ -819,7 +819,8 @@ } let certinfo = { 'upload_key': upload_key, - 'upload_token': upload_token + 'upload_token': upload_token, + 'app_info': analyseappinfo }; if (analyseappinfo.storage === 1) { // eslint-disable-next-line no-unused-vars,no-unreachable diff --git a/fir_client/src/components/user/FirUserProfileInfo.vue b/fir_client/src/components/user/FirUserProfileInfo.vue index 1024ea6..f3f10e7 100644 --- a/fir_client/src/components/user/FirUserProfileInfo.vue +++ b/fir_client/src/components/user/FirUserProfileInfo.vue @@ -247,7 +247,8 @@ -
+

绑定第三方账户 diff --git a/fir_client/src/utils/index.js b/fir_client/src/utils/index.js index 35eebe7..23684e2 100644 --- a/fir_client/src/utils/index.js +++ b/fir_client/src/utils/index.js @@ -125,6 +125,24 @@ export function dataURLtoFile(dataurl, filename) {//将base64转换为文件 return new File([u8arr], filename, {type: mime}); } +function get_file_type(upload_key) { + if (upload_key) { + const key_list = upload_key.split('.'); + if (key_list.length > 2) { + return key_list[key_list.length - 2] + } + } +} + +function get_filename(app_info, upload_key) { + if (app_info) { + let app_type = app_info.type; + if (app_type === 'iOS' || app_info === 'Android') { + return app_info.appname + '-' + app_info.version + '-' + app_info.short + '.' + get_file_type(upload_key) + } + } +} + export function uploadaliyunoss(file, certinfo, app, successcallback, processcallback) { let token = certinfo.upload_token; let uploadFileClient = new app.oss({ @@ -163,12 +181,19 @@ export function uploadaliyunoss(file, certinfo, app, successcallback, processcal uploadFileClient = client; } - const options = { + let options = { progress, parallel: 10, partSize: 1024 * 1024, timeout: 600000, }; + let filename = get_filename(certinfo.app_info, certinfo.upload_key); + if (filename) { + options['headers'] = { + 'Content-Disposition': 'attachment; filename="' + encodeURIComponent(filename) + '"', + 'Cache-Control': '' + } + } if (currentCheckpoint) { options.checkpoint = currentCheckpoint; diff --git a/fir_ser/api/utils/baseutils.py b/fir_ser/api/utils/baseutils.py index 97d7bc1..dc824ad 100644 --- a/fir_ser/api/utils/baseutils.py +++ b/fir_ser/api/utils/baseutils.py @@ -214,3 +214,17 @@ def check_app_password(app_password, password): if app_password.lower() != password.strip().lower(): return None return True + + +def get_filename_form_file(filename): + file_id_list = filename.split('.') + if file_id_list[-1] in ['ipa', 'apk']: + app_release_obj = AppReleaseInfo.objects.filter(release_id='.'.join(file_id_list[0:-1])).first() + if app_release_obj: + app_obj = app_release_obj.app_id + if app_obj.type == 0: + f_type = '.apk' + else: + f_type = '.ipa' + return f"{app_obj.name}-{app_release_obj.app_version}-{app_obj.short}{f_type}" + return filename diff --git a/fir_ser/api/utils/storage/aliyunApi.py b/fir_ser/api/utils/storage/aliyunApi.py index 749241a..e4b7f90 100644 --- a/fir_ser/api/utils/storage/aliyunApi.py +++ b/fir_ser/api/utils/storage/aliyunApi.py @@ -15,6 +15,10 @@ import re import hashlib import time import logging +from oss2 import SizedFileAdapter, determine_part_size +from oss2.models import PartInfo + +from api.utils.baseutils import get_filename_form_file logger = logging.getLogger(__name__) @@ -197,10 +201,13 @@ class AliYunOss(object): 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': 'attachment; filename="%s"' % filename + 'Content-Disposition': 'attachment; filename="%s"' % filename, + 'Cache-Control': '' } self.bucket.put_object_from_file(filename, local_file_full_path, headers) # with open(local_file_full_path, 'rb') as fileobj: @@ -218,3 +225,39 @@ class AliYunOss(object): if not os.path.exists(dir_path): os.makedirs(dir_path) return self.bucket.get_object_to_file(name, local_file_full_path) + + 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") diff --git a/fir_ser/api/utils/utils.py b/fir_ser/api/utils/utils.py index 154b917..cd423ba 100644 --- a/fir_ser/api/utils/utils.py +++ b/fir_ser/api/utils/utils.py @@ -7,7 +7,7 @@ import os, datetime, random import binascii from fir_ser.settings import SERVER_DOMAIN, CAPTCHA_LENGTH, MEDIA_ROOT, CACHE_KEY_TEMPLATE from api.models import APPSuperSignUsedInfo, APPToDeveloper, \ - UDIDsyncDeveloper, UserInfo, AppReleaseInfo, AppScreenShot, Token, DeveloperDevicesID + UDIDsyncDeveloper, UserInfo, AppReleaseInfo, AppScreenShot, Token, DeveloperDevicesID, Apps from api.utils.storage.caches import get_app_d_count_by_app_id from api.utils.storage.localApi import LocalStorage from api.utils.storage.storage import Storage @@ -15,7 +15,6 @@ from api.utils.tempcaches import TmpCache from api.utils.TokenManager import generate_numeric_token_of_length, generate_alphanumeric_token_of_length, make_token, \ verify_token from api.utils.sendmsg.sendmsg import SendMessage -from django.db.models import Sum from captcha.models import CaptchaStore from captcha.helpers import captcha_image_url from api.utils.storage.caches import consume_user_download_times diff --git a/fir_ser/api/views/download.py b/fir_ser/api/views/download.py index 822c58b..3895e94 100644 --- a/fir_ser/api/views/download.py +++ b/fir_ser/api/views/download.py @@ -21,12 +21,23 @@ from api.utils.serializer import AppsShortSerializer from api.models import Apps, AppReleaseInfo, APPToDeveloper, APPSuperSignUsedInfo from django.http import FileResponse import logging -from api.utils.baseutils import get_profile_full_path, get_app_domain_name +from api.utils.baseutils import get_profile_full_path, get_app_domain_name, get_filename_form_file from api.utils.throttle import VisitShortThrottle, InstallShortThrottle, InstallThrottle1, InstallThrottle2 logger = logging.getLogger(__name__) +def file_response(stream, filename, content_type): + return FileResponse(stream, as_attachment=True, + filename=filename, + content_type=content_type) + + +def mobileprovision_file_response(file_path): + return file_response(open(file_path, 'rb'), make_random_uuid() + '.mobileprovision', + "application/x-apple-aspen-config") + + class DownloadView(APIView): """ 文件下载接口,适用于本地存储和所有plist文件下载 @@ -64,10 +75,7 @@ class DownloadView(APIView): ios_plist_bytes = make_resigned(storage.get_download_url(filename.split('.')[0] + ".ipa"), storage.get_download_url(release_obj.icon_url), bundle_id, app_version, name) - response = FileResponse(ios_plist_bytes) - response['Content-Type'] = "application/x-plist" - response['Content-Disposition'] = 'attachment; filename=' + make_random_uuid() - return response + return file_response(ios_plist_bytes, make_random_uuid(), "application/x-plist") res.msg = "plist release_id error" elif f_type == 'mobileconifg': release_obj = AppReleaseInfo.objects.filter(release_id=filename.split('.')[0]).first() @@ -76,10 +84,8 @@ class DownloadView(APIView): app_obj = release_obj.app_id ios_udid_mobile_config = make_sign_udid_mobile_config(udid_url, app_obj.app_id, app_obj.bundle_id, app_obj.name) - response = FileResponse(ios_udid_mobile_config) - response['Content-Type'] = "application/x-apple-aspen-config" - response['Content-Disposition'] = 'attachment; filename=' + make_random_uuid() + '.mobileconfig' - return response + return file_response(ios_udid_mobile_config, make_random_uuid() + '.mobileconfig', + "application/x-apple-aspen-config") res.msg = "mobile_config release_id error" elif f_type == 'mobileprovision': release_obj = AppReleaseInfo.objects.filter(release_id=filename.split('.')[0]).first() @@ -90,51 +96,34 @@ class DownloadView(APIView): if not app_super_obj: file_path = settings.DEFAULT_MOBILEPROVISION.get("supersign").get('path') - if os.path.isfile(file_path): - response = FileResponse(open(file_path, 'rb')) - else: - response = FileResponse() + if file_path and os.path.isfile(file_path): + return mobileprovision_file_response(file_path) else: developer_obj = app_super_obj.developerid file_path = get_profile_full_path(developer_obj, release_obj.app_id) if os.path.isfile(file_path): - response = FileResponse(open(file_path, 'rb')) + return mobileprovision_file_response(file_path) else: file_path = settings.DEFAULT_MOBILEPROVISION.get("supersign").get('path') - if os.path.isfile(file_path): - response = FileResponse(open(file_path, 'rb')) - else: - response = FileResponse() - response['Content-Type'] = "application/x-apple-aspen-config" - response['Content-Disposition'] = 'attachment; filename=' + make_random_uuid() + '.mobileprovision' - return response + if file_path and os.path.isfile(file_path): + return mobileprovision_file_response(file_path) + res.msg = "mobile_provision release_id error" - elif f_type == 'dmobileprovision': + elif f_type == 'dmobileprovision': # 企业签名安装信任跳转 release_obj = AppReleaseInfo.objects.filter(release_id=filename.split('.')[0]).first() if release_obj: - file_path = settings.DEFAULT_MOBILEPROVISION.get("supersign").get('path') - if os.path.isfile(file_path): - response = FileResponse(open(file_path, 'rb')) - else: - response = FileResponse() - response['Content-Type'] = "application/x-apple-aspen-config" - response['Content-Disposition'] = 'attachment; filename=' + make_random_uuid() + '.mobileprovision' - return response + file_path = settings.DEFAULT_MOBILEPROVISION.get("enterprise").get('path') + if file_path and os.path.isfile(file_path): + return mobileprovision_file_response(file_path) res.msg = "d_mobile_provision release_id error" - else: file_path = os.path.join(settings.MEDIA_ROOT, filename) try: if os.path.isfile(file_path): - response = FileResponse(open(file_path, 'rb')) - else: - response = FileResponse() + return FileResponse(open(file_path, 'rb'), as_attachment=True, + filename=get_filename_form_file(filename)) except Exception as e: logger.error(f"read {file_path} failed Exception:{e}") - response = FileResponse() - response['content_type'] = "application/octet-stream" - response['Content-Disposition'] = 'attachment; filename=' + filename - return response res.code = 1004 res.msg = "token校验失败" diff --git a/fir_ser/cli.py b/fir_ser/cli.py index 111745f..3c1102d 100644 --- a/fir_ser/cli.py +++ b/fir_ser/cli.py @@ -10,7 +10,10 @@ import random import re import json import zipfile -from requests_toolbelt import MultipartEncoder + +from oss2 import determine_part_size, SizedFileAdapter +from oss2.models import PartInfo +from requests_toolbelt.multipart.encoder import MultipartEncoderMonitor import oss2 import requests from androguard.core.bytecodes import apk @@ -24,24 +27,65 @@ pip install androguard ''' -def upload_aliyunoss(access_key_id, access_key_secret, security_token, endpoint, bucket, file_path, file_name): +def progress(percent, width=50): + if percent >= 1: + percent = 1 + show_str = ('[%%-%ds]' % width) % (int(width * percent) * '#') + print('\r%s %d%% ' % (show_str, int(100 * percent)), file=sys.stdout, flush=True, end='') + + +def local_upload_callback(monitor): + progress(monitor.bytes_read / monitor.len, width=100) + + +def qiniu_progress_callback(upload_size, total_size): + progress(upload_size / total_size, width=100) + + +def alioss_progress_callback_fun(offset, total_size): + def progress_callback(upload_size, now_part_size): + percent = (offset + upload_size) / total_size # 接收的比例 + progress(percent, width=100) + + return progress_callback + + +def upload_aliyunoss(access_key_id, access_key_secret, security_token, endpoint, bucket, local_file_full_path, filename, + headers=None): stsauth = oss2.StsAuth(access_key_id, access_key_secret, security_token) bucket = oss2.Bucket(stsauth, endpoint, bucket) - with open(file_path, 'rb') as fileobj: - bucket.put_object(file_name, fileobj) + total_size = os.path.getsize(local_file_full_path) + # determine_part_size方法用于确定分片大小。 + part_size = determine_part_size(total_size, preferred_size=1024 * 1024 * 10) + upload_id = 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) + result = bucket.upload_part(filename, upload_id, part_number, + SizedFileAdapter(f, num_to_upload), + progress_callback=alioss_progress_callback_fun(offset, total_size)) + parts.append(PartInfo(part_number, result.etag)) + offset += num_to_upload + part_number += 1 + bucket.complete_multipart_upload(filename, upload_id, parts) print("数据上传存储成功.") def upload_qiniuyunoss(key, token, file_path): from qiniu import put_file - ret, info = put_file(token, key, file_path) + ret, info = put_file(token, key, file_path, progress_handler=qiniu_progress_callback) if info.status_code == 200: print("数据上传存储成功.") else: raise AssertionError(info.text) -class FLY_CLI_SER(object): +class FLYCliSer(object): def __init__(self, fly_cli_domain, fly_cli_token): self.fly_cli_domain = fly_cli_domain @@ -72,7 +116,7 @@ class FLY_CLI_SER(object): def upload_local_storage(self, upload_key, upload_token, app_id, file_path): url = '%s/api/v2/fir/server/upload' % self.fly_cli_domain - m = MultipartEncoder(fields={ + m = MultipartEncoderMonitor.from_fields(fields={ 'file': (os.path.basename(file_path), open(file_path, 'rb'), 'application/octet-stream'), 'certinfo': json.dumps( { @@ -82,7 +126,7 @@ class FLY_CLI_SER(object): 'app_id': app_id, } ), - }) + }, callback=local_upload_callback) header = { 'Content-Type': m.content_type, } @@ -99,6 +143,16 @@ class FLY_CLI_SER(object): icon_path = appobj.make_app_png(icon_path=appinfo.get("icon_path", None)) bundle_id = appinfo.get("bundle_id") upcretsdata = self.get_upload_token(bundle_id, appinfo.get("type")) + if appinfo.get('iOS', '') == 'iOS': + f_type = '.ipa' + else: + f_type = '.apk' + filename = "%s-%s-%s%s" % (appinfo.get('labelname'), appinfo.get('version'), upcretsdata.get('short'), f_type) + headers = { + 'Content-Disposition': 'attachment; filename="%s"' % filename.encode( + "utf-8").decode("latin1"), + 'Cache-Control': '' + } if upcretsdata['storage'] == 1: # 七牛云存储 upload_qiniuyunoss(upcretsdata['png_key'], upcretsdata['png_token'], icon_path) @@ -113,7 +167,7 @@ class FLY_CLI_SER(object): file_auth = upcretsdata['upload_token'] upload_aliyunoss(file_auth['access_key_id'], file_auth['access_key_secret'], file_auth['security_token'], - file_auth['endpoint'], file_auth['bucket'], app_path, upcretsdata['upload_key']) + file_auth['endpoint'], file_auth['bucket'], app_path, upcretsdata['upload_key'], headers) elif upcretsdata['storage'] == 3: # 本地存储 self.upload_local_storage(upcretsdata['png_key'], upcretsdata['png_token'], upcretsdata['app_uuid'], @@ -267,9 +321,9 @@ class AppInfo(object): if __name__ == '__main__': - fly_cli_domain = 'https://fly.harmonygames.cn' - fly_cli_token = '4baf717aff0011ea80c000163e069e27x2gRvmf2035rP3YWITJxvF4VVE32M55AuVHxOvSbTgdwgF54UeXFzrNLkP1lgUID' - fly_obj = FLY_CLI_SER(fly_cli_domain, fly_cli_token) + fly_cli_domain = 'https://app.hehelucky.cn' + fly_cli_token = 'f63d05d2bb0511eb9fa400163e069e27oTa51Q3N6rJT5yjddCrkmgiFeKRQNa0L3TctPLTYd0CrdIqgO3wfYCKsFnJysQXJ' + fly_obj = FLYCliSer(fly_cli_domain, fly_cli_token) if len(sys.argv) != 2: raise ValueError('参数有误') app_path = sys.argv[1] diff --git a/fir_ser/config.py b/fir_ser/config.py index 42c8b52..d8397cf 100644 --- a/fir_ser/config.py +++ b/fir_ser/config.py @@ -3,10 +3,12 @@ # project: 7月 # author: NinEveN # date: 2021/7/19 +import os API_DOMAIN = "https://app.hehelucky.cn" WEB_DOMAIN = "https://app.hehelucky.cn" -MOBILEPROVISION = "https://ali-static.jappstore.com/embedded2.mobileprovision" +MOBILEPROVISION = "https://ali-static.jappstore.com/embedded3.mobileprovision" +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) class BASECONF(object): @@ -231,12 +233,14 @@ class IPACONF(object): # 默认描述文件路径或者下载路径,用户企业签名或者超级签名 跳转 [设置 - 通用 - 描述文件|设备管理] 页面 # 如果配置了path路径,则走路径,如果配置了url,则走URL,path 优先级大于url优先级 'enterprise': { - 'url': MOBILEPROVISION + 'url': MOBILEPROVISION, + # 'path': os.path.join(BASE_DIR,'files', 'embedded.mobileprovision'), }, 'supersign': { # 超级签名,如果self 为True,则默认用自己的描述文件,否则同企业配置顺序一致,自己的配置文件有时候有问题 'self': False, - 'url': MOBILEPROVISION + 'url': MOBILEPROVISION, + # 'path': os.path.join(BASE_DIR,'files', 'embedded.mobileprovision'), } } diff --git a/fir_ser/files/embedded.mobileprovision b/fir_ser/files/embedded.mobileprovision index e729446..5006a2b 100644 Binary files a/fir_ser/files/embedded.mobileprovision and b/fir_ser/files/embedded.mobileprovision differ