From a41613b3e8bb94710fd91e9f9247ef76d5848bbb Mon Sep 17 00:00:00 2001 From: lan Date: Mon, 14 Aug 2023 13:30:10 +0800 Subject: [PATCH] add:fronted --- apps/base/depends.py | 49 +++++++++++++++++ apps/base/models.py | 2 +- apps/base/pydantics.py | 5 ++ apps/base/utils.py | 9 ++- apps/base/views.py | 64 ++++++++-------------- core/response.py | 15 +++++ core/storage.py | 2 +- core/utils.py | 2 + fcb-fronted/components.d.ts | 2 - fcb-fronted/src/components/FileBox.vue | 3 +- fcb-fronted/src/components/UploadFile.vue | 10 ++-- fcb-fronted/src/components/UploadText.vue | 52 ++++++++++-------- fcb-fronted/src/utils/request.ts | 23 +++++--- fcb-fronted/src/views/HomeView.vue | 27 ++++----- filecodebox.db-wal | Bin 0 -> 296672 bytes main.py | 11 +++- 16 files changed, 176 insertions(+), 100 deletions(-) create mode 100644 apps/base/depends.py create mode 100644 core/response.py diff --git a/apps/base/depends.py b/apps/base/depends.py new file mode 100644 index 0000000..e768266 --- /dev/null +++ b/apps/base/depends.py @@ -0,0 +1,49 @@ +# @Time : 2023/8/14 12:20 +# @Author : Lan +# @File : depends.py +# @Software: PyCharm +from typing import Union +from datetime import datetime, timedelta + +from fastapi import Header, HTTPException, Request + +from core.response import APIResponse + + +async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None): + return False + + +class IPRateLimit: + def __init__(self, count, minutes): + self.ips = {} + self.count = count + self.minutes = minutes + + def check_ip(self, ip): + # 检查ip是否被禁止 + if ip in self.ips: + if self.ips[ip]['count'] >= self.count: + if self.ips[ip]['time'] + timedelta(minutes=self.minutes) > datetime.now(): + return False + else: + self.ips.pop(ip) + return True + + def add_ip(self, ip): + ip_info = self.ips.get(ip, {'count': 0, 'time': datetime.now()}) + ip_info['count'] += 1 + ip_info['time'] = datetime.now() + self.ips[ip] = ip_info + return ip_info['count'] + + async def remove_expired_ip(self): + for ip in list(self.ips.keys()): + if self.ips[ip]['time'] + timedelta(minutes=self.minutes) < datetime.now(): + self.ips.pop(ip) + + def __call__(self, request: Request): + ip = request.headers.get('X-Real-IP', request.headers.get('X-Forwarded-For', request.client.host)) + if not self.check_ip(ip): + raise HTTPException(status_code=423, detail=f"请求次数过多,请稍后再试") + return ip diff --git a/apps/base/models.py b/apps/base/models.py index 58d88bb..a50af1e 100644 --- a/apps/base/models.py +++ b/apps/base/models.py @@ -28,7 +28,7 @@ class FileCodes(Model): created_at: Optional[datetime] = fields.DatetimeField(auto_now_add=True, description='创建时间') async def is_expired(self): - if self.expired_at: + if self.expired_at and (self.expired_count == -1 or self.used_count < self.expired_count): return self.expired_at < await get_now() else: return self.expired_count != -1 and self.used_count >= self.expired_count diff --git a/apps/base/pydantics.py b/apps/base/pydantics.py index e69de29..b5d677d 100644 --- a/apps/base/pydantics.py +++ b/apps/base/pydantics.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class SelectFileModel(BaseModel): + code: str diff --git a/apps/base/utils.py b/apps/base/utils.py index 5c5ed06..ced0e0b 100644 --- a/apps/base/utils.py +++ b/apps/base/utils.py @@ -7,6 +7,7 @@ import uuid import os from fastapi import UploadFile +from apps.base.depends import IPRateLimit from apps.base.models import FileCodes from core.utils import get_random_num, get_random_string @@ -41,7 +42,6 @@ async def get_expire_info(expire_value: int, expire_style: str): :return: expired_at 过期时间, expired_count 可用次数, used_count 已用次数, code 随机码 """ expired_count, used_count, now, code = -1, 0, datetime.datetime.now(), None - if expire_style == 'day': expired_at = now + datetime.timedelta(days=expire_value) elif expire_style == 'hour': @@ -58,6 +58,7 @@ async def get_expire_info(expire_value: int, expire_style: str): expired_at = now + datetime.timedelta(days=1) if not code: code = await get_random_code() + print(expire_style, expire_value, expired_at, expired_count, used_count, code) return expired_at, expired_count, used_count, code @@ -70,3 +71,9 @@ async def get_random_code(style='num'): code = await get_random_num() if style == 'num' else await get_random_string() if not await FileCodes.filter(code=code).exists(): return code + + +# 错误IP限制器 +error_ip_limit = IPRateLimit(1, 1) +# 上传文件限制器 +upload_ip_limit = IPRateLimit(10, 1) diff --git a/apps/base/views.py b/apps/base/views.py index b1f27fa..55aefe1 100644 --- a/apps/base/views.py +++ b/apps/base/views.py @@ -2,11 +2,12 @@ # @Author : Lan # @File : views.py # @Software: PyCharm -from fastapi import APIRouter, Form, UploadFile, File -from pydantic import BaseModel +from fastapi import APIRouter, Form, UploadFile, File, Depends from apps.base.models import FileCodes -from apps.base.utils import get_expire_info, get_file_path_name +from apps.base.pydantics import SelectFileModel +from apps.base.utils import get_expire_info, get_file_path_name, error_ip_limit +from core.response import APIResponse from core.storage import file_storage share_api = APIRouter( @@ -27,13 +28,9 @@ async def share_text(text: str = Form(...), expire_value: int = Form(default=1, size=len(text), prefix='文本分享' ) - return { - 'code': 200, - 'msg': 'success', - 'data': { - 'code': code, - } - } + return APIResponse(detail={ + 'code': code, + }) @share_api.post('/file/') @@ -52,40 +49,25 @@ async def share_file(expire_value: int = Form(default=1, gt=0), expire_style: st expired_count=expired_count, used_count=used_count, ) - return { - 'code': 200, - 'msg': 'success', - 'data': { - 'code': code, - 'name': file.filename, - } - } - - -class SelectFileModel(BaseModel): - code: str + return APIResponse(detail={ + 'code': code, + 'name': file.filename, + }) @share_api.post('/select/') -async def select_file(data: SelectFileModel): +async def select_file(data: SelectFileModel, ip: str = Depends(error_ip_limit)): file_code = await FileCodes.filter(code=data.code).first() if not file_code: - return { - 'code': 404, - 'msg': '文件不存在', - } + error_ip_limit.add_ip(ip) + return APIResponse(code=404, detail='文件不存在') if await file_code.is_expired(): - return { - 'code': 403, - 'msg': '文件已过期', - } - return { - 'code': 200, - 'msg': 'success', - 'data': { - 'code': file_code.code, - 'name': file_code.prefix + file_code.suffix, - 'size': file_code.size, - 'text': await file_storage.get_file_url(file_code), - } - } + return APIResponse(code=403, detail='文件已过期') + file_code.used_count += 1 + await file_code.save() + return APIResponse(detail={ + 'code': file_code.code, + 'name': file_code.prefix + file_code.suffix, + 'size': file_code.size, + 'text': await file_storage.get_file_url(file_code), + }) diff --git a/core/response.py b/core/response.py new file mode 100644 index 0000000..f6b8a38 --- /dev/null +++ b/core/response.py @@ -0,0 +1,15 @@ +# @Time : 2023/8/14 11:48 +# @Author : Lan +# @File : response.py +# @Software: PyCharm +from typing import Generic, TypeVar + +from pydantic.v1.generics import GenericModel + +T = TypeVar('T') + + +class APIResponse(GenericModel, Generic[T]): + code: int = 200 + message: str = 'ok' + detail: T diff --git a/core/storage.py b/core/storage.py index 4c66e36..88027af 100644 --- a/core/storage.py +++ b/core/storage.py @@ -63,4 +63,4 @@ class S3FileStorage: return result -file_storage = S3FileStorage() +file_storage = SystemFileStorage() diff --git a/core/utils.py b/core/utils.py index f6a96cd..ab856b9 100644 --- a/core/utils.py +++ b/core/utils.py @@ -6,6 +6,8 @@ import datetime import random import string +from apps.base.depends import IPRateLimit + async def get_random_num(): """ diff --git a/fcb-fronted/components.d.ts b/fcb-fronted/components.d.ts index a736e4a..6bd7b25 100644 --- a/fcb-fronted/components.d.ts +++ b/fcb-fronted/components.d.ts @@ -11,11 +11,9 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCol: typeof import('element-plus/es')['ElCol'] - ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] - ElModal: typeof import('element-plus/es')['ElModal'] ElOption: typeof import('element-plus/es')['ElOption'] ElProgress: typeof import('element-plus/es')['ElProgress'] ElRadio: typeof import('element-plus/es')['ElRadio'] diff --git a/fcb-fronted/src/components/FileBox.vue b/fcb-fronted/src/components/FileBox.vue index 566cfcf..49ddb11 100644 --- a/fcb-fronted/src/components/FileBox.vue +++ b/fcb-fronted/src/components/FileBox.vue @@ -50,7 +50,8 @@ const copyText = (text: any, style = 0) => {
{{ value.code }} - 点击下载 + + 点击下载 点击复制
diff --git a/fcb-fronted/src/components/UploadFile.vue b/fcb-fronted/src/components/UploadFile.vue index acb6a3d..556b921 100644 --- a/fcb-fronted/src/components/UploadFile.vue +++ b/fcb-fronted/src/components/UploadFile.vue @@ -11,8 +11,8 @@ const props = defineProps({ type: Object, default: () => { return { - expireValue: 1, - expireStyle: 'day', + expire_value: 1, + expire_style: 'day', } } } @@ -36,8 +36,8 @@ const handleHttpRequest = (options: any) => { fileBoxStore.showFileBox = true; const formData = new FormData(); formData.append('file', options.file); - formData.append('expireValue', props.shareData.expireValue); - formData.append('expireStyle', props.shareData.expireStyle); + formData.append('expire_value', props.shareData.expireValue); + formData.append('expire_style', props.shareData.expireStyle); request( { url: "share/file/", @@ -54,7 +54,7 @@ const handleHttpRequest = (options: any) => { } } ).then((res: any) => { - const data = res.data.data; + const data = res.detail; fileStore.shareData.forEach((file: any) => { if (file.uid === options.file.uid) { file.status = 'success'; diff --git a/fcb-fronted/src/components/UploadText.vue b/fcb-fronted/src/components/UploadText.vue index c4a60b8..246a64e 100644 --- a/fcb-fronted/src/components/UploadText.vue +++ b/fcb-fronted/src/components/UploadText.vue @@ -11,35 +11,39 @@ const props = defineProps({ type: Object, default: () => { return { - expireValue: 1, - expireStyle: 'day', + expire_value: 1, + expire_style: 'day', } } } }) const handleSubmitShareText = ()=>{ - const formData = new FormData(); - formData.append('text', shareText.value); - formData.append('expireValue', props.shareData.expireValue); - formData.append('expireStyle', props.shareData.expireStyle); - request({ - 'url': 'share/text/', - 'method': 'post', - 'data': formData, - }).then((res: any) => { - const data = res.data.data; - fileBoxStore.showFileBox = true; - fileStore.addShareData({ - 'name': '文本分享', - 'text': data.text, - 'code': data.code, - 'status': 'success', - 'percentage': 100, - 'size': shareText.value.length, - 'type': 'text', - 'uid': Date.now(), - }) - }) + if (shareText.value === '') { + alert('请输入您要分享的文本'); + } else { + const formData = new FormData(); + formData.append('text', shareText.value); + formData.append('expire_value', props.shareData.expireValue); + formData.append('expire_style', props.shareData.expireStyle); + request({ + 'url': 'share/text/', + 'method': 'post', + 'data': formData, + }).then((res: any) => { + const data = res.detail; + fileBoxStore.showFileBox = true; + fileStore.addShareData({ + 'name': '文本分享', + 'text': data.text, + 'code': data.code, + 'status': 'success', + 'percentage': 100, + 'size': shareText.value.length, + 'type': 'text', + 'uid': Date.now(), + }) + }); + } } diff --git a/fcb-fronted/src/utils/request.ts b/fcb-fronted/src/utils/request.ts index 374d415..fecfba2 100644 --- a/fcb-fronted/src/utils/request.ts +++ b/fcb-fronted/src/utils/request.ts @@ -2,17 +2,24 @@ import axios from "axios"; const instance = axios.create({ - baseURL: "", + baseURL: "http://localhost:12345", timeout: 6000000, headers:{ 'Authorization':localStorage.getItem('auth') } }); +// 对响应进行拦截 +instance.interceptors.response.use( + (response:any) => { + if (response.data.code === 200) { + return response.data; + } else { + alert(response.data.detail); + return Promise.reject(response.data); + } + }, (error:any) => { + alert(error.response.data.detail); + return Promise.reject(error); + }); -const Request = axios.create({ - baseURL: "http://localhost:12345", - timeout: 6000000, -}); - -export const request = Request; -export const http = instance; +export const request = instance; diff --git a/fcb-fronted/src/views/HomeView.vue b/fcb-fronted/src/views/HomeView.vue index 87ba778..cdd9f27 100644 --- a/fcb-fronted/src/views/HomeView.vue +++ b/fcb-fronted/src/views/HomeView.vue @@ -33,24 +33,21 @@ watch(code, (newVal) => { 'code': newVal } }).then((res: any) => { + fileBoxStore.showFileBox = true; + let flag = true; + fileStore.receiveData.forEach((file: any) => { + if (file.code === res.detail.code) { + flag = false; + return; + } + }); + if (flag) { + fileStore.addReceiveData(res.detail); + } + }).finally(() => { input_status.readonly = false; input_status.loading = false; code.value = ''; - if (res.data.code === 200) { - fileBoxStore.showFileBox = true; - let flag = true; - fileStore.receiveData.forEach((file: any) => { - if (file.code === res.data.data.code) { - flag = false; - return; - } - }); - if (flag) { - fileStore.addReceiveData(res.data.data); - } - } else { - alert(res.data.msg||'未知错误') - } }); } }); diff --git a/filecodebox.db-wal b/filecodebox.db-wal index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5c7411d262ed7d021ceb0ff6f157b7fc82de8eac 100644 GIT binary patch literal 296672 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