From a5b73c3695152d380232082ae3dcf0359bad2d77 Mon Sep 17 00:00:00 2001 From: nineven Date: Sat, 14 May 2022 00:33:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AD=98=E5=82=A8=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=8E=86=E5=8F=B2=E7=89=88=E6=9C=AC=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=AE=BE=E5=A4=87=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=88=87=E6=8D=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/apps/FirAppInfosbaseinfo.vue | 5 +- fir_client/src/components/apps/FirApps.vue | 6 +- .../src/components/user/FirSuperSignBase.vue | 14 +- .../src/components/user/FirUserStorage.vue | 788 +++++++++++++++--- fir_client/src/restful/index.js | 24 + fir_client/src/utils/index.js | 2 +- fir_ser/api/base_views.py | 13 +- .../api/migrations/0006_auto_20220513_0843.py | 60 ++ fir_ser/api/models.py | 34 +- fir_ser/api/urls.py | 4 +- fir_ser/api/utils/apputils.py | 10 +- fir_ser/api/utils/modelutils.py | 48 +- fir_ser/api/utils/serializer.py | 75 +- fir_ser/api/utils/utils.py | 17 +- fir_ser/api/views/apps.py | 12 +- fir_ser/api/views/storage.py | 270 +++++- fir_ser/api/views/uploads.py | 45 +- fir_ser/common/base/magic.py | 44 +- fir_ser/common/core/sysconfig.py | 25 +- fir_ser/common/libs/storage/aliyunApi.py | 9 + fir_ser/common/libs/storage/localApi.py | 10 + fir_ser/common/libs/storage/qiniuApi.py | 10 + fir_ser/common/notify/notify.py | 12 +- fir_ser/common/utils/caches.py | 2 +- fir_ser/common/utils/storage.py | 7 + fir_ser/config.py | 10 + fir_ser/xsign/utils/supersignutils.py | 3 +- fir_ser/xsign/views/supersign.py | 26 +- 28 files changed, 1409 insertions(+), 176 deletions(-) create mode 100644 fir_ser/api/migrations/0006_auto_20220513_0843.py diff --git a/fir_client/src/components/apps/FirAppInfosbaseinfo.vue b/fir_client/src/components/apps/FirAppInfosbaseinfo.vue index 2d329d0..9441b29 100644 --- a/fir_client/src/components/apps/FirAppInfosbaseinfo.vue +++ b/fir_client/src/components/apps/FirAppInfosbaseinfo.vue @@ -12,9 +12,10 @@ - - + + + diff --git a/fir_client/src/components/apps/FirApps.vue b/fir_client/src/components/apps/FirApps.vue index 7ba3463..7acb684 100644 --- a/fir_client/src/components/apps/FirApps.vue +++ b/fir_client/src/components/apps/FirApps.vue @@ -746,7 +746,11 @@ export default { loading.close(); }, { 'methods': 'POST', - 'data': {"bundleid": analyseappinfo.bundleid, "type": analyseappinfo.type} + 'data': { + "bundleid": analyseappinfo.bundleid, + "type": analyseappinfo.type, + "filesize": analyseappinfo.filesize + } }); } else { diff --git a/fir_client/src/components/user/FirSuperSignBase.vue b/fir_client/src/components/user/FirSuperSignBase.vue index e5c0863..370d0da 100644 --- a/fir_client/src/components/user/FirSuperSignBase.vue +++ b/fir_client/src/components/user/FirSuperSignBase.vue @@ -1310,15 +1310,18 @@ v-model="uidsearch" clearable placeholder="输入用户UID" - style="width: 30%;margin-right: 30px;margin-bottom: 10px"/> - + style="width: 30%;margin-right: 20px;margin-bottom: 10px"/> + + + 搜索 设备共享 -
+
共享签名池设备数量:{{ balance_info.all_balance }} 已经使用:【 {{ balance_info.used_balance }} 】 @@ -2071,9 +2074,10 @@ export default { this.cantransfer = false } else { this.cantransfer = true + this.transferInfo = {} this.$message.error("查询失败 " + data.msg) } - }, {'methods': 'PUT', data: {uid: uid}}) + }, {'methods': 'PUT', data: {uid: uid, 'act': 'check'}}) }, get_developer_uid(uid) { if (uid && uid.indexOf(':') > -1) { @@ -2493,6 +2497,7 @@ export default { this.iosdevicebillFun({"methods": "GET", "data": data}) } else if (tabname === "transferbill") { data.uidsearch = this.uidsearch.replace(/^\s+|\s+$/g, ""); + data.operatestatus = this.operatestatus; this.deviceTransferFun({"methods": "GET", "data": data}) } else if (tabname === "devicesrank") { if (this.timerangesearch && this.timerangesearch.length === 2) { @@ -2636,6 +2641,7 @@ export default { DeviceTransferBillInfo(data => { if (data.code === 1000) { this.transfer_bill_lists = data.data; + this.operate_status_choices = data.status_choices this.pagination.total = data.count; if (data.balance_info) { this.balance_info = data.balance_info; diff --git a/fir_client/src/components/user/FirUserStorage.vue b/fir_client/src/components/user/FirUserStorage.vue index 6963921..93172cf 100644 --- a/fir_client/src/components/user/FirUserStorage.vue +++ b/fir_client/src/components/user/FirUserStorage.vue @@ -38,8 +38,14 @@ - + + + @@ -61,7 +67,23 @@
+ + + + + + + + 存储空间最大容量 + {{ diskSize(editstorageinfo.max_storage_capacity * 1024 * 1024) }} + + + + + @@ -72,10 +94,109 @@
+ + + + + + + + + + + + + + + 取消 + + + + 存储共享指的是 将您的私有存储共享给目标用户使用 + + + + + + + + 查询校验 + + + + + + + 用户昵称: {{ shareInfo.name }} + 共享存储大小: {{ diskSize(shareInfo.number) }} + + + + 共 + {{ showshareList.length }} + 个私有存储被共享,点击查看详情 + + + + + + + + + + + 共享目标用户存储空间 {{ target_number }}MB + + + + + + + + + + + + 确定共享 + 取消 + + - + 存储选择: +
- 迁移数据并保存 - 强制迁移,忽略数据迁移失败错误,可能会导致数据丢失
- - 清理所有app数据 - - - 清理app历史版本数据,只保留最新数据 - + + + + + + + + + +
+ + + 存储最大容量空间 {{ diskSize(storageinfo.max_storage_capacity) }} + + + 存储已经使用容量空间 {{ diskSize(storageinfo.used_number) }} + 可用容量{{ diskSize(storageinfo.max_storage_capacity - storageinfo.used_number) }} + + + @@ -178,19 +309,29 @@
+
+ + + + 搜索 + +
- + width="160"> + + + + + + + + + + + + + + + - - - + width="100"> + width="150"> diff --git a/fir_client/src/restful/index.js b/fir_client/src/restful/index.js index 63a5719..b9d05f7 100644 --- a/fir_client/src/restful/index.js +++ b/fir_client/src/restful/index.js @@ -390,6 +390,30 @@ export function cleanStorageData(callBack, params) { ); } +/**用户存储清理操作 */ +export function shareStorageData(callBack, params) { + getData( + params.methods, + USERSEVER + '/storage/share', + params.data, + data => { + callBack(data); + } + ); +} + +/**用户存储清理操作 */ +export function configStorageData(callBack, params) { + getData( + params.methods, + USERSEVER + '/storage/config', + params.data, + data => { + callBack(data); + } + ); +} + /**用户个人信息 */ export function userinfos(callBack, params) { getData( diff --git a/fir_client/src/utils/index.js b/fir_client/src/utils/index.js index 1158012..da1b12d 100644 --- a/fir_client/src/utils/index.js +++ b/fir_client/src/utils/index.js @@ -626,7 +626,7 @@ export function diskSize(num) { let k = 1024; //设定基础容量大小 let sizeStr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; //容量单位 let i = 0; //单位下标和次幂 - for (let l = 0; l < 8; l++) { //因为只有8个单位所以循环八次 + for (let l = 0; l < 9; l++) { //因为只有8个单位所以循环八次 if (num / Math.pow(k, l) < 1) { //判断传入数值 除以 基础大小的次幂 是否小于1,这里小于1 就代表已经当前下标的单位已经不合适了所以跳出循环 break; //小于1跳出循环 } diff --git a/fir_ser/api/base_views.py b/fir_ser/api/base_views.py index 72c280b..d6ed57f 100644 --- a/fir_ser/api/base_views.py +++ b/fir_ser/api/base_views.py @@ -10,7 +10,8 @@ from api.models import AppReleaseInfo, UserInfo, AppScreenShot, AppStorage from api.utils.response import BaseResponse from api.utils.signalutils import run_delete_app_signal from api.utils.utils import delete_local_files, delete_app_screenshots_files, change_storage_and_change_head_img, \ - migrating_storage_data, clean_storage_data, check_storage_is_new_storage + migrating_storage_data, clean_storage_data, check_storage_is_new_storage, migrating_storage_file_data +from common.base.magic import MagicCacheData from common.cache.state import MigrateStorageState from common.utils.caches import del_cache_response_by_short, del_cache_by_delete_app, \ del_cache_storage @@ -50,6 +51,7 @@ def app_delete(app_obj): has_combo.has_combo = None has_combo.save(update_fields=['has_combo']) + MagicCacheData.invalid_cache(app_obj.app_id) app_obj.delete() return res @@ -101,12 +103,19 @@ def app_release_delete(app_obj, release_id, storage): return res +def check_storage_ok(user_obj, new_storage_obj): + if migrating_storage_file_data(user_obj, 'head_img.jpeg', new_storage_obj, False): + return True + + def storage_change(use_storage_id, user_obj, force): if use_storage_id: if user_obj.storage and use_storage_id == user_obj.storage.id: return True try: if use_storage_id == -1: + if not check_storage_ok(user_obj, None): + return False change_storage_and_change_head_img(user_obj, None) if migrating_storage_data(user_obj, None, False): if check_storage_is_new_storage(user_obj, None): @@ -114,6 +123,8 @@ def storage_change(use_storage_id, user_obj, force): UserInfo.objects.filter(pk=user_obj.pk).update(storage=None) else: new_storage_obj = AppStorage.objects.filter(pk=use_storage_id).first() + if not check_storage_ok(user_obj, new_storage_obj): + return False change_storage_and_change_head_img(user_obj, new_storage_obj) if migrating_storage_data(user_obj, new_storage_obj, False): if check_storage_is_new_storage(user_obj, new_storage_obj): diff --git a/fir_ser/api/migrations/0006_auto_20220513_0843.py b/fir_ser/api/migrations/0006_auto_20220513_0843.py new file mode 100644 index 0000000..6d9c1c2 --- /dev/null +++ b/fir_ser/api/migrations/0006_auto_20220513_0843.py @@ -0,0 +1,60 @@ +# Generated by Django 3.2.3 on 2022-05-13 08:43 + +import django.db.models.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0005_auto_20220412_1744'), + ] + + operations = [ + migrations.AddField( + model_name='appstorage', + name='max_storage_capacity', + field=models.BigIntegerField(default=0, verbose_name='存储最大使用容量,单位btype'), + ), + migrations.AddField( + model_name='userinfo', + name='storage_capacity', + field=models.BigIntegerField(default=0, verbose_name='存储容量,单位byte'), + ), + migrations.AlterField( + model_name='userinfo', + name='storage', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, + related_name='app_storage', to='api.appstorage', verbose_name='存储'), + ), + migrations.AlterUniqueTogether( + name='appstorage', + unique_together={('user_id', 'bucket_name', 'endpoint'), ('user_id', 'name')}, + ), + migrations.CreateModel( + name='StorageShareInfo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', + models.SmallIntegerField(choices=[(1, '共享成功'), (2, '取消共享')], default=0, help_text='1 共享成功 2 取消共享', + verbose_name='状态')), + ('number', models.BigIntegerField(default=0, verbose_name='容量大小,单位byte')), + ('description', models.CharField(blank=True, default='', max_length=128, verbose_name='操作描述')), + ('remote_addr', models.GenericIPAddressField(verbose_name='远程IP地址')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='添加时间')), + ('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('storage_id', models.ForeignKey(on_delete=django.db.models.fields.CharField, to='api.appstorage', + verbose_name='存储ID')), + ('to_user_id', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='storage_to_user_id', to=settings.AUTH_USER_MODEL, + verbose_name='用户ID')), + ('user_id', + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storage_org_user_id', + to=settings.AUTH_USER_MODEL, verbose_name='用户ID')), + ], + options={ + 'verbose_name': '私有存储共享', + 'verbose_name_plural': '私有存储共享', + }, + ), + ] diff --git a/fir_ser/api/models.py b/fir_ser/api/models.py index 7636961..4cd9317 100644 --- a/fir_ser/api/models.py +++ b/fir_ser/api/models.py @@ -40,11 +40,12 @@ class UserInfo(AbstractUser): memo = models.TextField('备注', blank=True, null=True, default=None, ) date_joined = models.DateTimeField(auto_now_add=True, verbose_name="注册时间") download_times = models.PositiveIntegerField(default=0, verbose_name="可用下载次数,需要用户充值") + storage_capacity = models.BigIntegerField(default=0, verbose_name="存储容量,单位byte") all_download_times = models.BigIntegerField(default=0, verbose_name="总共下载次数") default_domain_name = models.ForeignKey(to="DomainCnameInfo", verbose_name="默认下载页域名", on_delete=models.CASCADE) history_release_limit = models.IntegerField(default=10, verbose_name="app 历史记录版本", blank=True, null=True) - storage = models.OneToOneField(to='AppStorage', related_name='app_storage', - on_delete=models.SET_NULL, verbose_name="存储", null=True, blank=True) + storage = models.ForeignKey(to='AppStorage', related_name='app_storage', + on_delete=models.SET_NULL, verbose_name="存储", null=True, blank=True) api_token = models.CharField(max_length=256, verbose_name='api访问密钥', default='') notify_available_downloads = models.IntegerField(default=0, verbose_name="下载余额不足通知", blank=True, null=True) notify_available_signs = models.IntegerField(default=0, verbose_name="签名余额不足通知", blank=True, null=True) @@ -261,6 +262,8 @@ class AppStorage(models.Model): verbose_name="阿里云下载授权方式") cnd_auth_key = models.CharField(max_length=128, blank=True, null=True, verbose_name="阿里云cnd_auth_key") + max_storage_capacity = models.BigIntegerField(verbose_name="存储最大使用容量,单位btype", default=0) + created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") description = models.TextField('备注', blank=True, null=True, default='') @@ -268,6 +271,7 @@ class AppStorage(models.Model): class Meta: verbose_name = '存储配置' verbose_name_plural = "存储配置" + unique_together = (('user_id', 'name'), ('user_id', 'bucket_name', 'endpoint')) def save(self, *args, **kwargs): if self.storage_type in (1, 2): @@ -574,3 +578,29 @@ class UserPersonalConfig(BaseConfig): def __str__(self): return "%s-%s" % (self.key, self.description) + + +class StorageShareInfo(models.Model): + """ + 用户共享存储空间,可将该用户私有存储共享给 其他用户使用 + """ + user_id = models.ForeignKey(to=UserInfo, verbose_name="用户ID", on_delete=models.CASCADE, + related_name='storage_org_user_id') + to_user_id = models.ForeignKey(to=UserInfo, verbose_name="用户ID", on_delete=models.CASCADE, + related_name='storage_to_user_id', null=True, blank=True) + storage_id = models.ForeignKey(to=AppStorage, verbose_name="存储ID", on_delete=models.CharField) + status_choices = ((1, '共享成功'), (2, '取消共享')) + status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="状态", + help_text="1 共享成功 2 取消共享") + number = models.BigIntegerField(verbose_name="容量大小,单位byte", default=0) + description = models.CharField(verbose_name="操作描述", max_length=128, default='', blank=True) + remote_addr = models.GenericIPAddressField(verbose_name="远程IP地址") + created_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间") + updated_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = '私有存储共享' + verbose_name_plural = "私有存储共享" + + def __str__(self): + return f"{self.user_id}-{self.to_user_id}—{self.description}" diff --git a/fir_ser/api/urls.py b/fir_ser/api/urls.py index 89b9748..29846f6 100644 --- a/fir_ser/api/urls.py +++ b/fir_ser/api/urls.py @@ -29,7 +29,7 @@ from api.views.notify import NotifyReceiverView, NotifyConfigView, NotifyInfoVie from api.views.order import PriceView, OrderView, PaySuccess, OrderSyncView from api.views.personalconfig import PersonalConfigView from api.views.report import ReportView -from api.views.storage import StorageView, CleanStorageView +from api.views.storage import StorageView, CleanStorageView, ShareStorageView, StorageConfigView from api.views.thirdlogin import ValidWxChatToken, ThirdWxAccount from api.views.uploads import AppAnalyseView, UploadView @@ -48,6 +48,8 @@ urlpatterns = [ re_path("^apps$", AppsView.as_view()), re_path("^storage$", StorageView.as_view()), re_path("^storage/clean$", CleanStorageView.as_view()), + re_path("^storage/share$", ShareStorageView.as_view()), + re_path("^storage/config$", StorageConfigView.as_view()), re_path(r"^apps/(?P\w+)", AppInfoView.as_view()), re_path(r"^download_password/(?P\w+)", AppDownloadTokenView.as_view()), re_path(r"^appinfos/(?P\w+)/(?P\w+)", AppReleaseInfoView.as_view()), diff --git a/fir_ser/api/utils/apputils.py b/fir_ser/api/utils/apputils.py index a465b98..b76b68b 100644 --- a/fir_ser/api/utils/apputils.py +++ b/fir_ser/api/utils/apputils.py @@ -9,6 +9,7 @@ import random from api.models import AppReleaseInfo, Apps from api.utils.modelutils import get_user_domain_name from common.base.baseutils import make_app_uuid +from common.base.magic import MagicCacheData from common.cache.state import MigrateStorageState from common.utils.caches import del_cache_response_by_short from common.utils.storage import Storage @@ -78,9 +79,10 @@ def clean_history_apps(app_obj, user_obj, history_release_limit=20): storage_obj.delete_file(release_obj.release_id, app_obj.type) storage_obj.delete_file(release_obj.icon_url) release_obj.delete() + MagicCacheData.invalid_cache(app_obj.app_id) -def save_app_infos(app_file_name, user_obj, app_info, bundle_id, app_img, short, size, issupersign): +def save_app_infos(app_tmp_filename, app_file_name, user_obj, app_info, bundle_id, app_img, short, size, issupersign): app_uuid = make_app_uuid(user_obj, bundle_id + app_file_name.split(".")[1]) ##判断是否存在该app app_obj = Apps.objects.filter(app_id=app_uuid, user_id=user_obj).first() @@ -123,9 +125,13 @@ def save_app_infos(app_file_name, user_obj, app_info, bundle_id, app_img, short, app_obj.name = app_info["labelname"] app_obj.save(update_fields=["name", "bundle_id", "issupersign"]) del_cache_response_by_short(app_obj.app_id) - + MagicCacheData.invalid_cache(app_obj.app_id) AppReleaseInfo.objects.filter(app_id=app_obj).update(is_master=False) + file_info = storage.get_file_info(app_tmp_filename) + logger.info(f'get file {app_tmp_filename} info:{file_info}') + size = file_info.get('content_length', size) + release_data = { "app_id": app_obj, "icon_url": app_img, diff --git a/fir_ser/api/utils/modelutils.py b/fir_ser/api/utils/modelutils.py index 1ba542f..79c077b 100644 --- a/fir_ser/api/utils/modelutils.py +++ b/fir_ser/api/utils/modelutils.py @@ -13,9 +13,11 @@ from django.db.models import Count from rest_framework.pagination import PageNumberPagination from api.models import AppReleaseInfo, UserDomainInfo, DomainCnameInfo, UserAdDisplayInfo, RemoteClientInfo, \ - AppBundleIdBlackList, NotifyReceiver, WeChatInfo, AppDownloadToken + AppBundleIdBlackList, NotifyReceiver, WeChatInfo, AppDownloadToken, Apps, StorageShareInfo, UserInfo from common.base.baseutils import get_server_domain_from_request, get_user_default_domain_name, get_real_ip_address, \ get_origin_domain_name +from common.base.magic import MagicCacheData +from common.core.sysconfig import Config logger = logging.getLogger(__name__) @@ -248,3 +250,47 @@ def check_app_access_token(app_id, access_token, only_check, udid): return True if udid: return AppDownloadToken.objects.filter(app_id__app_id=app_id, bind_udid=udid).count() + + +@MagicCacheData.make_cache(60 * 60 * 24, key=lambda x: x.app_id) +def get_app_storage_used(app_obj): + binary_size_sum = 0 + for release_obj in AppReleaseInfo.objects.filter(app_id=app_obj).all(): + if release_obj.release_type == 0: + sign_count = 0 + else: + sign_count = AppReleaseInfo.objects.filter( + app_id__apptodeveloper__release_file=release_obj.release_id).values( + 'app_id__apptodeveloper__binary_file').distinct().count() + binary_size_sum += release_obj.binary_size * (sign_count + 1) + return binary_size_sum + + +# @MagicCacheData.make_cache(3600, key=lambda x: x.uid) +def get_user_storage_used(user_obj): + binary_size_sum = 0 + if isinstance(user_obj, UserInfo): + user_obj = [user_obj] + for app_obj in Apps.objects.filter(user_id__in=user_obj).all(): + binary_size_sum += get_app_storage_used(app_obj) + return binary_size_sum + + +def get_user_storage_capacity(user_obj): + storage_obj = user_obj.storage + if not storage_obj: + user_storage_capacity = user_obj.storage_capacity + storage_capacity = user_storage_capacity if user_storage_capacity else Config.STORAGE_FREE_CAPACITY + else: + if storage_obj.user_id == user_obj: + max_storage_capacity = storage_obj.max_storage_capacity + storage_capacity = max_storage_capacity if max_storage_capacity else Config.STORAGE_OSS_CAPACITY + else: + share_obj = StorageShareInfo.objects.filter(to_user_id=user_obj, status=1, storage_id=storage_obj).first() + if share_obj: + can_use_number = storage_obj.max_storage_capacity - get_user_storage_used(storage_obj.app_storage.all()) + storage_capacity = share_obj.number if share_obj.number < can_use_number else can_use_number + else: + storage_capacity = Config.STORAGE_FREE_CAPACITY + + return storage_capacity diff --git a/fir_ser/api/utils/serializer.py b/fir_ser/api/utils/serializer.py index 3a9c39e..f2597e1 100644 --- a/fir_ser/api/utils/serializer.py +++ b/fir_ser/api/utils/serializer.py @@ -2,10 +2,11 @@ import logging import os from rest_framework import serializers +from rest_framework.exceptions import ValidationError from api import models from api.utils.apputils import bytes2human -from api.utils.modelutils import get_user_domain_name, get_app_domain_name, get_app_download_uri +from api.utils.modelutils import get_user_domain_name, get_app_domain_name, get_app_download_uri, get_user_storage_used from common.base.baseutils import get_choices_dict, WeixinLoginUid from common.cache.storage import AdPicShowCache from common.core.sysconfig import Config @@ -62,7 +63,17 @@ class UserInfoSerializer(serializers.ModelSerializer): model = models.UserInfo fields = ["username", "uid", "mobile", "job", "email", "domain_name", "role", "first_name", 'head_img', 'storage_active', 'supersign_active', 'free_download_times', 'download_times', - 'certification', 'qrcode_domain_name'] + 'certification', 'qrcode_domain_name', 'storage_used', 'storage_capacity', 'storage_used_capacity'] + + storage_used_capacity = serializers.SerializerMethodField() + + def get_storage_used_capacity(self, obj): + return get_user_storage_used(obj) + + storage_used = serializers.SerializerMethodField() + + def get_storage_used(self, obj): + return bytes2human(self.get_storage_used_capacity(obj)) head_img = serializers.SerializerMethodField() @@ -128,13 +139,13 @@ class AppsSerializer(serializers.ModelSerializer): def get_preview_url(self, obj): return get_app_download_uri(None, obj.user_id, obj) - private_developer_number = serializers.IntegerField(default=0) - count = serializers.IntegerField(default=0) + # private_developer_number = serializers.IntegerField(default=0) + # count = serializers.IntegerField(default=0) # # def get_private_developer_number(self, obj): # return models.AppleDeveloperToAppUse.objects.filter(app_id=obj).count() # - private_developer_used_number = serializers.IntegerField(default=0) + # private_developer_used_number = serializers.IntegerField(default=0) # # def get_private_developer_used_number(self, obj): # return models.DeveloperDevicesID.objects.filter(app_id=obj, @@ -150,12 +161,12 @@ class AppsSerializer(serializers.ModelSerializer): def get_sign_type_choice(self, obj): return get_choices_dict(obj.supersign_type_choices) - supersign_used_number = serializers.IntegerField(default=0) + # supersign_used_number = serializers.IntegerField(default=0) # # def get_supersign_used_number(self, obj): # return models.APPSuperSignUsedInfo.objects.filter(app_id=obj).all().count() # - developer_used_count = serializers.IntegerField(default=0) + # developer_used_count = serializers.IntegerField(default=0) # # def get_developer_used_count(self, obj): # return models.DeveloperAppID.objects.filter(app_id=obj).all().count() @@ -354,9 +365,35 @@ class StorageSerializer(serializers.ModelSerializer): model = models.AppStorage exclude = ["user_id"] + def validate(self, attrs): + endpoint = attrs.get('endpoint', '') + storage_type = attrs.get('storage_type', '') + if storage_type == 2 and endpoint not in Config.STORAGE_ALLOW_ENDPOINT: + raise ValidationError(f'endpoint [{endpoint}] not in {Config.STORAGE_ALLOW_ENDPOINT}') + max_storage_capacity = attrs.get('max_storage_capacity', -1) + if max_storage_capacity != -1: + attrs['max_storage_capacity'] = max_storage_capacity * 1024 * 1024 + elif max_storage_capacity == 0: + attrs['max_storage_capacity'] = Config.STORAGE_OSS_CAPACITY + else: + del attrs['max_storage_capacity'] + return attrs + storage_type_display = serializers.CharField(source="get_storage_type_display", read_only=True) download_auth_type_choices = serializers.SerializerMethodField() + used = serializers.SerializerMethodField() + used_number = serializers.SerializerMethodField() + shared = serializers.SerializerMethodField() + + def get_used(self, obj): + return obj.app_storage.count() + + def get_shared(self, obj): + return models.StorageShareInfo.objects.filter(status=1, storage_id=obj).count() + + def get_used_number(self, obj): + return get_user_storage_used(obj.app_storage.all()) # secret_key = serializers.SerializerMethodField() # 加上此选项,会导致update获取不到值 @@ -519,3 +556,27 @@ class PersonalConfigSerializer(serializers.ModelSerializer): def get_title(self, obj): return getattr(Config, f'{obj.key}_DES') + + +class StorageShareSerializer(serializers.ModelSerializer): + class Meta: + model = models.StorageShareInfo + exclude = ["id", "remote_addr", "user_id", "to_user_id"] + + target_user = serializers.SerializerMethodField() + cancel = serializers.SerializerMethodField() + used = serializers.SerializerMethodField() + status_display = serializers.CharField(source='get_status_display') + storage_name = serializers.CharField(source='storage_id.name') + + def get_target_user(self, obj): + user_obj = obj.user_id + if self.get_cancel(obj): + user_obj = obj.to_user_id + return {'uid': user_obj.uid, 'name': user_obj.first_name} + + def get_used(self, obj): + return obj.to_user_id.storage_id == obj.storage_id.id and obj.status == 1 + + def get_cancel(self, obj): + return self.context.get('user_obj').pk == obj.user_id.pk diff --git a/fir_ser/api/utils/utils.py b/fir_ser/api/utils/utils.py index 78b7607..3d27f12 100644 --- a/fir_ser/api/utils/utils.py +++ b/fir_ser/api/utils/utils.py @@ -120,25 +120,28 @@ def migrating_storage_file_data(user_obj, filename, new_storage_obj, clean_old_d if old_storage_obj.get_storage_type() == 3: if new_storage_obj.get_storage_type() == 3: # 都是本地存储,无需操作 - pass + return True else: # 本地向云存储上传,并删除本地数据 - new_storage_obj.upload_file(local_file_full_path) + res = new_storage_obj.upload_file(local_file_full_path) if clean_old_data: - delete_local_files(filename) + return delete_local_files(filename) + return res else: if new_storage_obj.get_storage_type() == 3: # 云存储下载 本地,并删除云存储 if download_files_form_oss(old_storage_obj, local_file_full_path, True): if clean_old_data: - old_storage_obj.delete_file(filename) + return old_storage_obj.delete_file(filename) + return True else: # 云存储互传,先下载本地,然后上传新云存储,删除本地和老云存储 if download_files_form_oss(old_storage_obj, local_file_full_path, True): - new_storage_obj.upload_file(local_file_full_path) - delete_local_files(filename) + res1 = new_storage_obj.upload_file(local_file_full_path) + res2 = delete_local_files(filename) if clean_old_data: - old_storage_obj.delete_file(filename) + return old_storage_obj.delete_file(filename) + return res1 and res2 def migrating_storage_data(user_obj, new_storage_obj, clean_old_data): diff --git a/fir_ser/api/views/apps.py b/fir_ser/api/views/apps.py index 3a48145..cd548fe 100644 --- a/fir_ser/api/views/apps.py +++ b/fir_ser/api/views/apps.py @@ -14,12 +14,14 @@ from rest_framework.views import APIView from api.base_views import app_delete from api.models import Apps, AppReleaseInfo, UserInfo, AppScreenShot, AppDownloadToken -from api.utils.modelutils import get_user_domain_name, get_app_domain_name +from api.utils.apputils import bytes2human +from api.utils.modelutils import get_user_domain_name, get_app_domain_name, get_app_storage_used from api.utils.response import BaseResponse from api.utils.serializer import AppsSerializer, AppReleaseSerializer, AppsListSerializer, AppsQrListSerializer, \ AppDownloadTokenSerializer from api.utils.signalutils import run_delete_app_signal from api.utils.utils import delete_local_files, delete_app_screenshots_files +from common.base.magic import MagicCacheData from common.cache.state import MigrateStorageState from common.core.auth import ExpiringTokenAuthentication from common.utils.caches import del_cache_response_by_short, get_app_today_download_times, del_cache_by_delete_app @@ -63,7 +65,7 @@ class AppsPageNumber(PageNumberPagination): page_size = 20 # 每页显示多少条 page_size_query_param = 'size' # URL中每页显示条数的参数 page_query_param = 'page' # URL中页码的参数 - max_page_size = None # 最大页码数限制 + max_page_size = 100 # 最大页码数限制 class AppsView(APIView): @@ -120,6 +122,7 @@ class AppInfoView(APIView): if app_obj: app_serializer = AppsSerializer(app_obj, context={"storage": Storage(request.user)}) res.data = app_serializer.data + res.data['storage_used'] = bytes2human(get_app_storage_used(app_obj)) else: logger.error(f"app_id:{app_id} is not found in user:{request.user}") res.msg = "未找到该应用" @@ -268,10 +271,13 @@ class AppReleaseInfoView(APIView): has_combo.has_combo = None has_combo.save(update_fields=["has_combo"]) del_cache_response_by_short(has_combo.app_id) + MagicCacheData.invalid_cache(app_obj.app_id) app_obj.delete() else: pass - del_cache_response_by_short(app_obj.app_id) + if app_obj: + MagicCacheData.invalid_cache(app_obj.app_id) + del_cache_response_by_short(app_obj.app_id) return Response(res.dict) diff --git a/fir_ser/api/views/storage.py b/fir_ser/api/views/storage.py index a143576..3243dc8 100644 --- a/fir_ser/api/views/storage.py +++ b/fir_ser/api/views/storage.py @@ -6,43 +6,101 @@ import logging +from django.db.models import Q from rest_framework.response import Response from rest_framework.views import APIView from api.base_views import storage_change, app_delete -from api.models import AppStorage, Apps +from api.models import AppStorage, Apps, StorageShareInfo, UserInfo from api.utils.apputils import clean_history_apps +from api.utils.modelutils import PageNumber, get_user_storage_capacity, get_user_storage_used from api.utils.response import BaseResponse -from api.utils.serializer import StorageSerializer +from api.utils.serializer import StorageSerializer, StorageShareSerializer from api.utils.utils import upload_oss_default_head_img -from common.base.baseutils import get_choices_dict +from common.base.baseutils import get_choices_dict, get_choices_name_from_key, get_real_ip_address from common.cache.state import MigrateStorageState from common.core.auth import ExpiringTokenAuthentication, StoragePermission +from common.core.sysconfig import Config +from common.utils.caches import del_cache_storage logger = logging.getLogger(__name__) +def get_storage_group(request, res, storage_type, is_default=True): + use_storage_obj = request.user.storage + if use_storage_obj: + res.storage = use_storage_obj.id + else: + res.storage = -1 # 默认存储 + storage_group = [] + if is_default: + storage_group.append({'group_name': '默认存储', + 'storages': [{'id': -1, 'name': '默认存储', 'used': True if res.storage == -1 else False}]}) + for s_type in storage_type: + s_group = {'group_name': get_choices_name_from_key(AppStorage.storage_choices, s_type), 'storages': []} + + for obj in AppStorage.objects.filter(storage_type=s_type).filter( + Q(user_id=request.user) | Q(storageshareinfo__to_user_id=request.user, + storageshareinfo__status=1)).values('id', 'name', 'user_id__first_name', + 'user_id__uid').distinct(): + if obj['user_id__uid'] != request.user.uid: + ext = {'username': obj['user_id__first_name']} + else: + share_count = StorageShareInfo.objects.filter(storage_id=obj['id'], status=1).count() + share_used = StorageShareInfo.objects.filter(storage_id=obj['id'], status=1, + to_user_id__storage__id=obj['id']).count() + ext = {'share_count': share_count, 'share_used': share_used} + s_info = {'id': obj['id'], 'name': obj['name'], 'used': False, 'ext': ext} + if request.user.storage and request.user.storage.id == obj['id']: + s_info['used'] = True + if not is_default and obj['user_id__uid'] != request.user.uid: + continue + else: + s_group['storages'].append(s_info) + storage_group.append(s_group) + return storage_group + + class StorageView(APIView): authentication_classes = [ExpiringTokenAuthentication, ] permission_classes = [StoragePermission, ] + storage_type = [2] def get(self, request): res = BaseResponse() res.storage_list = [] storage_org_list = list(AppStorage.storage_choices) for storage_t in storage_org_list: - if storage_t[0] not in [0, 3]: + if storage_t[0] in self.storage_type: res.storage_list.append({'id': storage_t[0], 'name': storage_t[1]}) + res.endpoint_list = get_choices_dict([(x, x) for x in Config.STORAGE_ALLOW_ENDPOINT]) act = request.query_params.get("act", None) + is_default = request.query_params.get("is_default", 'true') + pk = request.query_params.get("pk", None) + keysearch = request.query_params.get("keysearch", None) if act == 'storage_type': res.download_auth_type_choices = get_choices_dict(AppStorage.download_auth_type_choices) + res.default_max_storage_capacity = Config.STORAGE_OSS_CAPACITY + return Response(res.dict) + + if act == 'storage_group': + res.storage_group_list = get_storage_group(request, res, self.storage_type, is_default == 'true') return Response(res.dict) # [1,2] 表示七牛存储和阿里云存储 - storage_obj = AppStorage.objects.filter(user_id=request.user, storage_type__in=[1, 2]) - if storage_obj: - storage_serializer = StorageSerializer(storage_obj, many=True) + storage_queruset = AppStorage.objects.filter(user_id=request.user, storage_type__in=self.storage_type) + if pk: + storage_queruset = storage_queruset.filter(pk=pk) + if keysearch: + storage_queruset = storage_queruset.filter(Q(name__contains=keysearch) | Q(bucket_name=keysearch)) + + if storage_queruset: + page_obj = PageNumber() + page_serializer = page_obj.paginate_queryset(queryset=storage_queruset.order_by("-created_time"), + request=request, + view=self) + storage_serializer = StorageSerializer(page_serializer, many=True) storage_data_lists = storage_serializer.data storage_lists = {} for storage_data in storage_data_lists: @@ -52,6 +110,9 @@ class StorageView(APIView): storage_lists[storage_type] = [] storage_lists[storage_type].append(storage_data) res.data = storage_data_lists + res.count = storage_queruset.count() + else: + res.data = [] use_storage_obj = request.user.storage if use_storage_obj: @@ -78,11 +139,11 @@ class StorageView(APIView): res.code = 1005 else: logger.info(f"user {request.user} add new storage failed") - res.msg = serializer.errors + res.msg = str(serializer.errors) res.code = 1005 else: logger.info(f"user {request.user} add new storage failed") - res.msg = serializer.errors + res.msg = str(serializer.errors) res.code = 1005 return Response(res.dict) @@ -93,6 +154,20 @@ class StorageView(APIView): use_storage_id = data.get("use_storage_id", None) force = data.get("force", None) if use_storage_id: + if use_storage_id != -1: + obj = AppStorage.objects.filter(pk=use_storage_id).first() + if obj: + if obj.user_id != request.user: + if not StorageShareInfo.objects.filter(to_user_id=request.user, status=1, + storage_id=obj).first(): + res.code = 1007 + res.msg = "数据异常,请重试" + return Response(res.dict) + else: + res.code = 1007 + res.msg = "数据异常,请重试" + return Response(res.dict) + with MigrateStorageState(request.user.uid) as state: if state: if not storage_change(use_storage_id, request.user, force): @@ -106,10 +181,10 @@ class StorageView(APIView): storage_id = data.get("id", None) if storage_id: - if request.user.storage and storage_id == request.user.storage.id: - res.msg = '存储正在使用中,无法修改' - res.code = 1007 - return Response(res.dict) + # if request.user.storage and storage_id == request.user.storage.id: + # res.msg = '存储正在使用中,无法修改' + # res.code = 1007 + # return Response(res.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}, @@ -120,6 +195,10 @@ class StorageView(APIView): if upload_oss_default_head_img(request.user, new_storage_obj): res.msg = serializer.validated_data logger.info(f"user {request.user} update storage success") + del_cache_storage(request.user) + for share_obj in StorageShareInfo.objects.filter(user_id=request.user, + storage_id=new_storage_obj, status=1).all(): + del_cache_storage(share_obj.user_id) else: storage_obj_bak.save() logger.error(f"user {request.user} update storage failed") @@ -127,10 +206,10 @@ class StorageView(APIView): res.code = 1005 else: logger.info(f"user {request.user} update storage failed") - res.msg = serializer.errors + res.msg = str(serializer.errors) res.code = 1005 else: - res.msg = serializer.errors + res.msg = str(serializer.errors) res.code = 1005 else: res.msg = '该存储不存在' @@ -176,3 +255,164 @@ class CleanStorageView(APIView): res.code = 1007 res.msg = "参数不合法,清理失败" return Response(res.dict) + + +class ShareStorageView(APIView): + authentication_classes = [ExpiringTokenAuthentication, ] + permission_classes = [StoragePermission, ] + + def get(self, request): + res = BaseResponse() + uidsearch = request.query_params.get("uidsearch", None) + status = request.query_params.get("operatestatus", '-1') + share_list = StorageShareInfo.objects.filter(Q(user_id=request.user) | Q(to_user_id=request.user)).distinct() + page_obj = PageNumber() + if uidsearch: + share_list = share_list.filter( + Q(user_id__uid=uidsearch) | Q(to_user_id__uid=uidsearch) | Q(storage_id__name__contains=uidsearch)) + try: + if status != '' and get_choices_name_from_key(StorageShareInfo.status_choices, int(status)): + share_list = share_list.filter(status=status) + except Exception as e: + logger.error(f'status {status} check failed Exception:{e}') + + app_page_serializer = page_obj.paginate_queryset(queryset=share_list.order_by("-created_time"), + request=request, + view=self) + app_serializer = StorageShareSerializer(app_page_serializer, many=True, context={'user_obj': request.user}) + res.data = app_serializer.data + res.count = share_list.count() + res.status_choices = get_choices_dict(StorageShareInfo.status_choices) + return Response(res.dict) + + def post(self, request): + res = BaseResponse() + uid = request.data.get('target_uid') + sid = request.data.get('target_sid') + number = request.data.get('target_number') + if uid and number and sid: + to_user_obj = UserInfo.objects.filter(uid=uid, is_active=True, supersign_active=True).first() + storage_obj = AppStorage.objects.filter(pk=sid, user_id=request.user).first() + if to_user_obj and storage_obj: + if isinstance(number, int) and 0 < number < 999999999: + number = number * 1024 * 1024 + + max_storage_capacity = storage_obj.max_storage_capacity + storage_capacity = max_storage_capacity if max_storage_capacity else Config.STORAGE_OSS_CAPACITY + number = number if number < storage_capacity else storage_capacity + user_obj = request.user + if user_obj.pk != to_user_obj.pk: + try: + if True: + share_obj = StorageShareInfo.objects.filter(user_id=user_obj, to_user_id=to_user_obj, + status=1, storage_id=storage_obj).first() + if share_obj: + # number += share_obj.number + share_obj.number = number if number < storage_capacity else storage_capacity + share_obj.remote_addr = get_real_ip_address(request) + share_obj.description = f'{user_obj.first_name} 共享给 {to_user_obj.first_name} {share_obj.number} Mb存储空间 ' + share_obj.save(update_fields=['number', 'remote_addr', 'description']) + else: + StorageShareInfo.objects.create(user_id=user_obj, to_user_id=to_user_obj, + status=1, number=number, + storage_id=storage_obj, + remote_addr=get_real_ip_address(request), + description=f'{user_obj.first_name} 共享给 {to_user_obj.first_name} {number} Mb存储空间') + return Response(res.dict) + else: + res.msg = f'私有存储空间余额不足,当前存储余额最大为 {all_balance}' + except Exception as e: + res.msg = str(e) + else: + res.msg = '用户不合法' + else: + res.msg = '共享数量异常' + else: + res.msg = '用户信息不存在' + else: + res.msg = '参数有误' + res.code = 1003 + return Response(res.dict) + + def put(self, request): + res = BaseResponse() + uid = request.data.get('uid') + sid = request.data.get('sid') + if uid: + user_obj = UserInfo.objects.filter(uid=uid, is_active=True, supersign_active=True).first() + if user_obj: + share_obj = StorageShareInfo.objects.filter(user_id=request.user, to_user_id__uid=uid, status=1).all() + info_list = [] + number = 0 + for obj in share_obj: + storage_obj = obj.storage_id + info_list.append( + { + 'storage_name': storage_obj.name, 'number': obj.number, + 'storage_id': storage_obj.pk, 'storage_access_key': storage_obj.access_key + }) + number += obj.number + res.data = {'uid': user_obj.uid, 'name': user_obj.first_name, "info_list": info_list, "number": number} + else: + res.msg = '用户信息不存在' + res.code = 1003 + else: + res.msg = '参数有误' + res.code = 1003 + return Response(res.dict) + + def delete(self, request): + res = BaseResponse() + uid = request.query_params.get("uid", None) + status = request.query_params.get("status", None) + number = request.query_params.get("number", None) + sid = request.query_params.get("sid", None) + if MigrateStorageState(request.user.uid).get_state(): + res.code = 1008 + res.msg = "数据迁移中,无法处理该操作" + return Response(res.dict) + if uid and status and number and sid: + share_obj = StorageShareInfo.objects.filter(user_id=request.user, to_user_id__uid=uid, status=status, + number=abs(int(number)), storage_id_id=sid).first() + if share_obj: + target_user_obj = UserInfo.objects.filter(uid=uid).first() + if target_user_obj and target_user_obj.storage: + if target_user_obj.storage.id == share_obj.storage_id.id: + app_obj_lists = Apps.objects.filter(user_id=target_user_obj).all() + for app_obj in app_obj_lists: + res = app_delete(app_obj) + logger.warning(f'clean share storage {target_user_obj} {app_obj} {res}') + target_user_obj.storage = None + target_user_obj.save(update_fields=['storage']) + share_obj.status = 2 + share_obj.save(update_fields=['status']) + else: + res.code = 1003 + res.msg = '共享记录不存在' + return Response(res.dict) + + +class StorageConfigView(APIView): + authentication_classes = [ExpiringTokenAuthentication, ] + permission_classes = [StoragePermission, ] + + def get(self, request): + res = BaseResponse() + res.data = { + 'user_max_storage_capacity': get_user_storage_capacity(request.user), + 'user_used_storage_capacity': get_user_storage_used(request.user), + 'user_history_limit': UserInfo.objects.filter(pk=request.user.pk).first().history_release_limit, + } + return Response(res.dict) + + def put(self, request): + history_release_limit = request.data.get('user_history_limit') + if history_release_limit: + try: + history_release_limit = int(history_release_limit) + except Exception as e: + logger.warning(f"update user history_release_limit failed Exception:{e}") + history_release_limit = request.user.history_release_limit + + UserInfo.objects.filter(pk=request.user.pk).update(history_release_limit=abs(history_release_limit)) + return self.get(request) diff --git a/fir_ser/api/views/uploads.py b/fir_ser/api/views/uploads.py index 49ea180..b81138a 100644 --- a/fir_ser/api/views/uploads.py +++ b/fir_ser/api/views/uploads.py @@ -12,7 +12,8 @@ from rest_framework.views import APIView from api.models import Apps, AppReleaseInfo, UserInfo, AppScreenShot, CertificationInfo, UserAdDisplayInfo from api.utils.apputils import get_random_short, save_app_infos -from api.utils.modelutils import get_app_download_uri, check_bundle_id_legal +from api.utils.modelutils import get_app_download_uri, check_bundle_id_legal, get_user_storage_used, \ + get_user_storage_capacity from api.utils.response import BaseResponse from api.utils.signalutils import run_signal_resign_utils from common.base.baseutils import make_app_uuid, make_from_user_uuid @@ -21,7 +22,7 @@ from common.core.auth import ExpiringTokenAuthentication from common.core.sysconfig import Config from common.utils.caches import upload_file_tmp_name, del_cache_response_by_short from common.utils.storage import Storage -from common.utils.token import verify_token +from common.utils.token import verify_token, make_token from fir_ser import settings logger = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class AppAnalyseView(APIView): # 1.接收 bundelid ,返回随机应用名称和短连接 bundle_id = request.data.get("bundleid", None) app_type = request.data.get("type", None) + filesize = request.data.get("filesize", 0) if bundle_id: if check_bundle_id_legal(request.user.uid, bundle_id): res.code = 1004 @@ -52,6 +54,17 @@ class AppAnalyseView(APIView): return Response(res.dict) if bundle_id and app_type: + storage_used = get_user_storage_used(request.user) + try: + filesize = abs(int(filesize)) + except Exception as e: + logger.warning(f"filesize check failed {request.data} Exception:{e}") + filesize = 0 + if filesize + storage_used > get_user_storage_capacity(request.user): + res.code = 1008 + res.msg = "存储空间不足,请升级存储空间或清理无用的历史版本数据来释放空间" + return Response(res.dict) + ap = 'apk' if app_type.lower() == 'iOS'.lower(): ap = 'ipa' @@ -94,7 +107,8 @@ class AppAnalyseView(APIView): "storage": storage_type, "is_new": is_new, "binary_url": binary_url, - "enable_sign": enable_sign + "enable_sign": enable_sign, + "access_token": make_token(app_uuid, time_limit=60 * 5, key='update_app_info', force_new=True) } if storage_type not in [1, 2]: res.data['domain_name'] = Config.FILE_UPLOAD_DOMAIN @@ -135,9 +149,21 @@ class AppAnalyseView(APIView): png_tmp_filename = data.get("png_key") png_new_filename = data.get("png_key").strip(settings.FILE_UPLOAD_TMP_KEY) - logger.info(f"user {request.user} create or update app {data.get('bundleid')} data:{data}") - if save_app_infos(app_new_filename, request.user, app_info, - data.get("bundleid"), png_new_filename, data.get("short"), data.get('filesize'), + + bundle_id = data.get("bundleid", "") + if not bundle_id: + raise KeyError('bundle_id not exist') + app_uuid = make_app_uuid(request.user, bundle_id + app_new_filename.split(".")[1]) + if not verify_token(data.get('access_token', ''), app_uuid, False): + res.msg = '授权过期,请重试' + res.code = 1004 + storage.delete_file(app_tmp_filename) + storage.delete_file(png_tmp_filename) + return Response(res.dict) + + logger.info(f"user {request.user} create or update app {bundle_id} data:{data}") + if save_app_infos(app_tmp_filename, app_new_filename, request.user, app_info, + bundle_id, png_new_filename, data.get("short"), data.get('filesize'), data.get('enable_sign')): # 需要将tmp 文件修改为正式文件 storage.rename_file(app_tmp_filename, app_new_filename) @@ -274,7 +300,6 @@ class UploadView(APIView): res.msg = "文件不存在" for file_obj in files: try: - app_type = file_obj.name.split(".")[-1] if app_type == "tmp": app_type = file_obj.name.split(".")[-2] @@ -287,6 +312,12 @@ class UploadView(APIView): res.msg = "错误的类型" return Response(res.dict) + storage_used = get_user_storage_used(request.user) + if file_obj.size + storage_used > get_user_storage_capacity(request.user): + res.code = 1008 + res.msg = "存储空间不足,请升级存储空间或清理无用的历史版本数据来释放空间" + return Response(res.dict) + random_file_name = make_from_user_uuid(request.user.uid) local_file = os.path.join(settings.MEDIA_ROOT, cert_info.get("upload_key", random_file_name)) # 读取传入的文件 diff --git a/fir_ser/common/base/magic.py b/fir_ser/common/base/magic.py index f87667a..3461e41 100644 --- a/fir_ser/common/base/magic.py +++ b/fir_ser/common/base/magic.py @@ -140,9 +140,9 @@ def magic_call_in_times(call_time=24 * 3600, call_limit=6, key=None): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): - cache_key = func.__name__ + cache_key = f'magic_call_in_times_{func.__name__}' if key: - cache_key = f'magic_call_in_times_{cache_key}_{key(*args, **kwargs)}' + cache_key = f'{cache_key}_{key(*args, **kwargs)}' cache_data = cache.get(cache_key) if cache_data: if cache_data > call_limit: @@ -169,3 +169,43 @@ def magic_call_in_times(call_time=24 * 3600, call_limit=6, key=None): return wrapper return decorator + + +class MagicCacheData(object): + @staticmethod + def make_cache(cache_time=60 * 10, key=None): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + cache_key = f'magic_cache_data' + if key: + cache_key = f'{cache_key}_{key(*args, **kwargs)}' + else: + cache_key = f'{cache_key}_{func.__name__}' + + res = cache.get(cache_key) + if res: + logger.info(f"exec {func} finished. cache_key:{cache_key} cache data exist result:{res}") + return res + else: + start_time = time.time() + try: + res = func(*args, **kwargs) + cache.set(cache_key, res, cache_time) + logger.info( + f"exec {func} finished. time:{time.time() - start_time} cache_key:{cache_key} result:{res}") + except Exception as e: + logger.info( + f"exec {func} failed. time:{time.time() - start_time} cache_key:{cache_key} Exception:{e}") + + return res + + return wrapper + + return decorator + + @staticmethod + def invalid_cache(key): + cache_key = f'magic_cache_data_{key}' + res = cache.delete(cache_key) + logger.warning(f"invalid_cache cache_key:{cache_key} result:{res}") diff --git a/fir_ser/common/core/sysconfig.py b/fir_ser/common/core/sysconfig.py index 6bff803..5631696 100644 --- a/fir_ser/common/core/sysconfig.py +++ b/fir_ser/common/core/sysconfig.py @@ -15,7 +15,7 @@ from rest_framework import serializers from api.models import SystemConfig, UserPersonalConfig from common.cache.storage import UserSystemConfigCache from config import BASECONF, THIRDLOGINCONF, AUTHCONF, IPACONF, DOWNLOADTIMESCONF, PAYCONF, STORAGEKEYCONF, SENDERCONF, \ - APPLEDEVELOPERCONF, DOMAINCONF, USERPERSONALCONFIGKEY, CONFIGDESCRIPTION + APPLEDEVELOPERCONF, DOMAINCONF, USERPERSONALCONFIGKEY, CONFIGDESCRIPTION, OSSSTORAGECONF logger = logging.getLogger(__name__) @@ -293,6 +293,27 @@ class UserDownloadTimesCache(ConfigCacheBase): def AUTH_USER_GIVE_DOWNLOAD_TIMES(self): return super().get_value('AUTH_USER_GIVE_DOWNLOAD_TIMES', DOWNLOADTIMESCONF.AUTH_USER_GIVE_DOWNLOAD_TIMES) + @property + def SIGN_EXTRA_MULTIPLE(self): + return super().get_value('SIGN_EXTRA_MULTIPLE', DOWNLOADTIMESCONF.SIGN_EXTRA_MULTIPLE) + + +class OssStorageConfCache(ConfigCacheBase): + def __init__(self, *args, **kwargs): + super(OssStorageConfCache, self).__init__(*args, **kwargs) + + @property + def STORAGE_ALLOW_ENDPOINT(self): + return super().get_value('STORAGE_ALLOW_ENDPOINT', OSSSTORAGECONF.STORAGE_ALLOW_ENDPOINT) + + @property + def STORAGE_FREE_CAPACITY(self): + return super().get_value('STORAGE_FREE_CAPACITY', OSSSTORAGECONF.STORAGE_FREE_CAPACITY) + + @property + def STORAGE_OSS_CAPACITY(self): + return super().get_value('STORAGE_OSS_CAPACITY', OSSSTORAGECONF.STORAGE_OSS_CAPACITY) + class PayConfCache(ConfigCacheBase): def __init__(self, *args, **kwargs): @@ -417,7 +438,7 @@ class ConfigDescriptionCache(ConfigCacheBase): class ConfigCache(BaseConfCache, DomainConfCache, IpaConfCache, AuthConfCache, UserDownloadTimesCache, GeeTestConfCache, PayConfCache, ThirdStorageConfCache, ThirdPartConfCache, AppleDeveloperConfCache, - UserPersonalConfKeyCache, ConfigDescriptionCache, WechatConfCache): + UserPersonalConfKeyCache, ConfigDescriptionCache, WechatConfCache, OssStorageConfCache): def __init__(self, *args, **kwargs): super(ConfigCache, self).__init__(*args, **kwargs) diff --git a/fir_ser/common/libs/storage/aliyunApi.py b/fir_ser/common/libs/storage/aliyunApi.py index 9a26de0..e16407c 100644 --- a/fir_ser/common/libs/storage/aliyunApi.py +++ b/fir_ser/common/libs/storage/aliyunApi.py @@ -223,6 +223,15 @@ class AliYunOss(object): 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) diff --git a/fir_ser/common/libs/storage/localApi.py b/fir_ser/common/libs/storage/localApi.py index caaa542..7a0b4d2 100644 --- a/fir_ser/common/libs/storage/localApi.py +++ b/fir_ser/common/libs/storage/localApi.py @@ -72,3 +72,13 @@ class LocalStorage(object): if os.path.isfile(local_file_full_path): return True return False + + @staticmethod + def get_file_info(name): + file_path = os.path.join(settings.MEDIA_ROOT, name) + base_info = {} + if os.path.isfile(file_path): + stat_info = os.stat(file_path) + base_info['content_length'] = stat_info.st_size + base_info['last_modified'] = stat_info.st_mtime + return base_info diff --git a/fir_ser/common/libs/storage/qiniuApi.py b/fir_ser/common/libs/storage/qiniuApi.py index 3264761..d316603 100644 --- a/fir_ser/common/libs/storage/qiniuApi.py +++ b/fir_ser/common/libs/storage/qiniuApi.py @@ -85,3 +85,13 @@ class QiNiuOss(object): except Exception as e: logger.error(f"check download file and move file {local_file_full_path} failed Exception {e}") return False + + def get_file_info(self, name): + bucket = BucketManager(self.qiniu_obj) + result = bucket.stat(self.bucket_name, name) + base_info = {} + if result.get('fsize', 0): + base_info['content_length'] = result.get('fsize', 0) + if result.get('putTime', 0): + base_info['last_modified'] = result.get('putTime', 0) // 10000000 + return base_info diff --git a/fir_ser/common/notify/notify.py b/fir_ser/common/notify/notify.py index 6322c23..d189d72 100644 --- a/fir_ser/common/notify/notify.py +++ b/fir_ser/common/notify/notify.py @@ -42,11 +42,7 @@ def pay_success_notify(user_obj, order_obj): notify_by_email(user_obj, message_type, get_pay_success_html_content(user_obj, order_obj)) -def get_magic_call_key(*args, **kwargs): - return args[0].uid - - -@magic_call_in_times(key=get_magic_call_key) +@magic_call_in_times(key=lambda *x: x[0].uid) def sign_failed_notify(user_obj, developer_obj, app_obj): """ 3, '应用签名失败' @@ -65,7 +61,7 @@ def sign_failed_notify(user_obj, developer_obj, app_obj): notify_by_email(user_obj, message_type, get_sign_failed_html_content(user_obj, app_obj, developer_obj, now_time)) -@magic_call_in_times(key=get_magic_call_key) +@magic_call_in_times(key=lambda *x: x[0].uid) def sign_unavailable_developer_notify(user_obj, app_obj): """ 3, '应用签名失败' @@ -82,7 +78,7 @@ def sign_unavailable_developer_notify(user_obj, app_obj): notify_by_email(user_obj, message_type, get_sign_unavailable_developer_html_content(user_obj, app_obj, now_time)) -@magic_call_in_times(key=get_magic_call_key) +@magic_call_in_times(key=lambda *x: x[0].uid) def sign_app_over_limit_notify(user_obj, app_obj, used_num, limit_number): """ 0, '签名余额不足' @@ -131,7 +127,7 @@ def check_developer_status_notify(user_obj, developer_obj_list, developer_used_i yesterday_used_number)) -@magic_call_in_times(key=get_magic_call_key) +@magic_call_in_times(key=lambda *x: x[0].uid) def download_times_not_enough(user_obj, msg): """ 1, '下载次数不足' diff --git a/fir_ser/common/utils/caches.py b/fir_ser/common/utils/caches.py index a3d5672..2ad60aa 100644 --- a/fir_ser/common/utils/caches.py +++ b/fir_ser/common/utils/caches.py @@ -242,7 +242,7 @@ def consume_user_download_times_by_app_obj(app_obj): user_id = app_obj.user_id_id auth_status = get_user_cert_auth_status(user_id) amount = get_app_d_count_by_app_id(app_obj.app_id) - if consume_user_download_times(user_id, app_obj.app_id, amount, auth_status): + if consume_user_download_times(user_id, app_obj.app_id, int(amount * Config.SIGN_EXTRA_MULTIPLE), auth_status): return False return True diff --git a/fir_ser/common/utils/storage.py b/fir_ser/common/utils/storage.py index 4dfc6e5..eeeefae 100644 --- a/fir_ser/common/utils/storage.py +++ b/fir_ser/common/utils/storage.py @@ -110,6 +110,13 @@ class Storage(object): except Exception as e: logger.error(f"rename {old_filename} to {new_filename} failed Exception {e}") + def get_file_info(self, name): + if self.storage: + try: + return self.storage.get_file_info(name) + except Exception as e: + logger.error(f"get file info {name} failed Exception {e}") + def upload_file(self, local_file_full_path): if self.storage: try: diff --git a/fir_ser/config.py b/fir_ser/config.py index aa93196..80f8b96 100644 --- a/fir_ser/config.py +++ b/fir_ser/config.py @@ -390,6 +390,7 @@ bIX1aWjPxirQX9mzaL3oEQI= class DOWNLOADTIMESCONF(object): + SIGN_EXTRA_MULTIPLE = 2 # 超级签名消耗额外倍数,超级签名需要占用的服务大量资源 USER_FREE_DOWNLOAD_TIMES = 5 AUTH_USER_FREE_DOWNLOAD_TIMES = 10 NEW_USER_GIVE_DOWNLOAD_TIMES = 100 @@ -459,3 +460,12 @@ class CONFIGDESCRIPTION(object): class USERPERSONALCONFIGKEY(object): DEVELOPER_STATUS_CONFIG = ['DEVELOPER_WAIT_ABNORMAL_STATE', 'DEVELOPER_WAIT_ABNORMAL_DEVICE', 'DEVELOPER_ABNORMAL_DEVICE_WRITE'] + + +class OSSSTORAGECONF(object): + STORAGE_FREE_CAPACITY = 2048 * 1024 * 1024 # 单位byte 2G + STORAGE_OSS_CAPACITY = 1024 * 1024 * 1024 * 1024 # 单位byte 1T + STORAGE_ALLOW_ENDPOINT = [ + 'oss-cn-beijing-internal.aliyuncs.com', + 'oss-cn-zhangjiakou-internal.aliyuncs.com' + ] diff --git a/fir_ser/xsign/utils/supersignutils.py b/fir_ser/xsign/utils/supersignutils.py index 117f2b5..bec1b8e 100644 --- a/fir_ser/xsign/utils/supersignutils.py +++ b/fir_ser/xsign/utils/supersignutils.py @@ -21,7 +21,7 @@ from api.utils.response import BaseResponse from api.utils.utils import delete_local_files, download_files_form_oss from common.base.baseutils import file_format_path, delete_app_profile_file, get_profile_full_path, format_apple_date, \ get_format_time, make_app_uuid, make_from_user_uuid, AesBaseCrypt -from common.base.magic import run_function_by_locker, call_function_try_attempts, magic_wrapper +from common.base.magic import run_function_by_locker, call_function_try_attempts, magic_wrapper, MagicCacheData from common.cache.state import CleanErrorBundleIdSignDataState from common.cache.storage import RedisCacheBase from common.constants import DeviceStatus, AppleDeveloperStatus, SignStatus @@ -615,6 +615,7 @@ class IosUtils(object): sign_status=SignStatus.PROFILE_DOWNLOAD_COMPLETE).update( sign_status=SignStatus.SIGNATURE_PACKAGE_COMPLETE) del_cache_response_by_short(app_obj.app_id) + MagicCacheData.invalid_cache(app_obj.app_id) return True def check_sign_permission(self): diff --git a/fir_ser/xsign/views/supersign.py b/fir_ser/xsign/views/supersign.py index 9f859d0..b1c91c5 100644 --- a/fir_ser/xsign/views/supersign.py +++ b/fir_ser/xsign/views/supersign.py @@ -656,12 +656,17 @@ class DeviceTransferBillView(APIView): def get(self, request): res = BaseResponse() uidsearch = request.query_params.get("uidsearch", None) - + status = request.query_params.get("operatestatus", '-1') user_used_list = IosDeveloperBill.objects.filter( Q(user_id=request.user) | Q(to_user_id=request.user)).distinct() page_obj = PageNumber() if uidsearch: user_used_list = user_used_list.filter(Q(user_id__uid=uidsearch) | Q(to_user_id__uid=uidsearch)) + try: + if status != '' and get_choices_name_from_key(IosDeveloperBill.status_choices, int(status)): + user_used_list = user_used_list.filter(status=status) + except Exception as e: + logger.error(f'status {status} check failed Exception:{e}') app_page_serializer = page_obj.paginate_queryset(queryset=user_used_list.order_by("-created_time"), request=request, @@ -674,6 +679,7 @@ class DeviceTransferBillView(APIView): 'used_balance': get_user_public_used_sign_num(request.user), 'all_balance': get_user_public_sign_num(request.user) } + res.status_choices = get_choices_dict(IosDeveloperBill.status_choices) return Response(res.dict) def post(self, request): @@ -726,6 +732,24 @@ class DeviceTransferBillView(APIView): uid = request.data.get("uid", None) status = request.data.get("status", None) number = request.data.get("number", None) + act = request.data.get("act", '') + if act == 'check': + if uid: + user_obj = UserInfo.objects.filter(uid=uid, is_active=True, supersign_active=True).first() + if user_obj: + bill_obj = IosDeveloperBill.objects.filter(user_id=request.user, to_user_id__uid=uid, + status=2).first() + number = 0 + if bill_obj: + number = bill_obj.number + res.data = {'uid': user_obj.uid, 'name': user_obj.first_name, "number": number} + else: + res.msg = '用户信息不存在' + res.code = 1003 + else: + res.msg = '参数有误' + res.code = 1003 + return Response(res.dict) if uid and status and number: if check_uid_has_relevant(uid, request.user.uid):