diff --git a/.gitignore b/.gitignore index d015956..49f0c04 100644 --- a/.gitignore +++ b/.gitignore @@ -80,7 +80,7 @@ docs/_build/ # PyBuilder .pybuilder/ target/ - +*.db # Jupyter Notebook .ipynb_checkpoints diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e4a73e5 --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1,4 @@ +# @Time : 2023/8/13 20:43 +# @Author : Lan +# @File : __init__.py.py +# @Software: PyCharm diff --git a/apps/base/__init__.py b/apps/base/__init__.py new file mode 100644 index 0000000..e4a73e5 --- /dev/null +++ b/apps/base/__init__.py @@ -0,0 +1,4 @@ +# @Time : 2023/8/13 20:43 +# @Author : Lan +# @File : __init__.py.py +# @Software: PyCharm diff --git a/apps/base/models.py b/apps/base/models.py new file mode 100644 index 0000000..b34f6c7 --- /dev/null +++ b/apps/base/models.py @@ -0,0 +1,38 @@ +# @Time : 2023/8/13 20:43 +# @Author : Lan +# @File : models.py +# @Software: PyCharm +from datetime import datetime +from typing import Optional + +from tortoise import fields +from tortoise.models import Model +from tortoise.contrib.pydantic import pydantic_model_creator + + +class FileCodes(Model): + id: Optional[int] = fields.IntField(pk=True) + code: Optional[int] = fields.CharField(description='分享码', max_length=255, index=True, unique=True) + prefix: Optional[str] = fields.CharField(max_length=255, description='前缀', default='') + suffix: Optional[str] = fields.CharField(max_length=255, description='后缀', default='') + uuid_file_name: Optional[str] = fields.CharField(max_length=255, description='uuid文件名') + file_path: Optional[str] = fields.CharField(max_length=255, description='文件路径') + size: Optional[int] = fields.IntField(description='文件大小', default=0) + + expired_at: Optional[datetime] = fields.DatetimeField(null=True, description='过期时间') + expired_count: Optional[int] = fields.IntField(description='可用次数', default=0) + used_count: Optional[int] = fields.IntField(description='已用次数', default=0) + + created_at: Optional[datetime] = fields.DatetimeField(auto_now_add=True, description='创建时间') + + async def is_expired(self): + if self.expired_at: + return self.expired_at < datetime.now() + else: + return self.expired_count != -1 and self.used_count >= self.expired_count + + async def get_file_path(self): + return f"{self.file_path}/{self.uuid_file_name}" + + +file_codes_pydantic = pydantic_model_creator(FileCodes, name='FileCodes') diff --git a/apps/base/pydantics.py b/apps/base/pydantics.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/base/utils.py b/apps/base/utils.py new file mode 100644 index 0000000..ccd6d1d --- /dev/null +++ b/apps/base/utils.py @@ -0,0 +1,72 @@ +# @Time : 2023/8/14 01:10 +# @Author : Lan +# @File : utils.py +# @Software: PyCharm +import datetime +import uuid +import os +from fastapi import UploadFile + +from apps.base.models import FileCodes +from core.utils import get_random_num, get_random_string + + +async def get_file_path_name(file: UploadFile): + """ + 获取文件路径和文件名 + :param file: + :return: { + 'path': 'share/data/2021/08/13', + 'suffix': '.jpg', + 'prefix': 'test', + 'file_uuid': '44a83bbd70e04c8aa7fd93bfd8c88249', + 'uuid_file_name': '44a83bbd70e04c8aa7fd93bfd8c88249.jpg', + 'save_path': 'share/data/2021/08/13/44a83bbd70e04c8aa7fd93bfd8c88249.jpg' + } + """ + today = datetime.datetime.now() + path = f"share/data/{today.strftime('%Y/%m/%d')}" + prefix, suffix = os.path.splitext(file.filename) + file_uuid = f"{uuid.uuid4().hex}" + uuid_file_name = f"{file_uuid}{suffix}" + save_path = f"{path}/{uuid_file_name}" + return path, suffix, prefix, uuid_file_name, file_uuid, save_path + + +async def get_expire_info(expire_value: int, expire_style: str): + """ + 获取过期信息 + :param expire_value: + :param expire_style: + :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': + expired_at = now + datetime.timedelta(hours=expire_value) + elif expire_style == 'minute': + expired_at = now + datetime.timedelta(minutes=expire_value) + elif expire_style == 'count': + expired_at = now + datetime.timedelta(days=1) + expired_count = expire_value + elif expire_style == 'forever': + expired_at = None + code = await get_random_code(style='string') + else: + expired_at = now + datetime.timedelta(days=1) + if not code: + code = await get_random_code() + return expired_at, expired_count, used_count, code + + +async def get_random_code(style='num'): + """ + 获取随机字符串 + :return: + """ + while True: + code = await get_random_num() if style == 'num' else await get_random_string() + if not await FileCodes.filter(code=code).exists(): + return code diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..bfdb9d4 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,4 @@ +# @Time : 2023/8/11 20:06 +# @Author : Lan +# @File : __init__.py.py +# @Software: PyCharm diff --git a/core/storage.py b/core/storage.py new file mode 100644 index 0000000..158f81a --- /dev/null +++ b/core/storage.py @@ -0,0 +1,64 @@ +# @Time : 2023/8/11 20:06 +# @Author : Lan +# @File : storage.py +# @Software: PyCharm +import asyncio +import aioboto3 +from fastapi import UploadFile +from pathlib import Path + +from apps.base.models import FileCodes + + +class SystemFileStorage: + def __init__(self): + self.chunk_size = 256 * 1024 + self.root_path = Path('./data') + + def _save(self, file, save_path): + with open(save_path, 'wb') as f: + chunk = file.read(self.chunk_size) + while chunk: + f.write(chunk) + chunk = file.read(self.chunk_size) + + async def save_file(self, file: UploadFile, save_path: str): + save_path = self.root_path / save_path + if not save_path.parent.exists(): + save_path.parent.mkdir(parents=True) + await asyncio.to_thread(self._save, file.file, save_path) + + async def delete_file(self, file_code: FileCodes): + save_path = self.root_path / await file_code.get_file_path() + if save_path.exists(): + save_path.unlink() + + async def get_file_url(self, file_code: FileCodes): + return f"/api/select?code={file_code.code}" + + +class S3FileStorage: + def __init__(self): + self.access_key_id = '' + self.secret_access_key = '' + self.bucket_name = '' + self.endpoint_url = '' + self.session = aioboto3.Session( + aws_access_key_id=self.access_key_id, aws_secret_access_key=self.secret_access_key + ) + + async def save_file(self, file: UploadFile, save_path: str): + async with self.session.client("s3", endpoint_url=self.endpoint_url) as s3: + await s3.put_object(Bucket=self.bucket_name, Key=save_path, Body=await file.read(), ContentType=file.content_type) + + async def delete_file(self, file_code: FileCodes): + async with self.session.client("s3", endpoint_url=self.endpoint_url) as s3: + await s3.delete_object(Bucket=self.bucket_name, Key=await file_code.get_file_path()) + + async def get_file_url(self, file_code: FileCodes): + async with self.session.client("s3", endpoint_url=self.endpoint_url) as s3: + result = await s3.generate_presigned_url('get_object', Params={'Bucket': self.bucket_name, 'Key': await file_code.get_file_path()}, ExpiresIn=3600) + return result + + +file_storage = SystemFileStorage() diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..47eab36 --- /dev/null +++ b/core/utils.py @@ -0,0 +1,25 @@ +# @Time : 2023/8/13 19:54 +# @Author : Lan +# @File : utils.py +# @Software: PyCharm +import random +import string + + +async def get_random_num(): + """ + 获取随机数 + :return: + """ + return random.randint(10000, 99999) + + +r_s = string.ascii_uppercase + string.digits + + +async def get_random_string(): + """ + 获取随机字符串 + :return: + """ + return ''.join(random.choice(r_s) for _ in range(5)) diff --git a/fcb-fronted/components.d.ts b/fcb-fronted/components.d.ts index 67ba4e4..6335a29 100644 --- a/fcb-fronted/components.d.ts +++ b/fcb-fronted/components.d.ts @@ -19,7 +19,6 @@ declare module 'vue' { ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] - ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElTag: typeof import('element-plus/es')['ElTag'] ElUpload: typeof import('element-plus/es')['ElUpload'] diff --git a/fcb-fronted/src/components/UploadFile.vue b/fcb-fronted/src/components/UploadFile.vue index a544861..b99a09b 100644 --- a/fcb-fronted/src/components/UploadFile.vue +++ b/fcb-fronted/src/components/UploadFile.vue @@ -81,7 +81,7 @@ function pasteLister(event: any) { for (let i = 0; i < items.length; i++) { if (items[i].kind === 'string') { if (items[i].type.match(/^text\/plain/)) { - items[i].getAsString(function(str) { + items[i].getAsString(function(str:any) { console.log(str); }); } diff --git a/fcb-fronted/src/stores/fileData.ts b/fcb-fronted/src/stores/fileData.ts index 9c469c6..2bd80a2 100644 --- a/fcb-fronted/src/stores/fileData.ts +++ b/fcb-fronted/src/stores/fileData.ts @@ -2,8 +2,8 @@ import { defineStore } from 'pinia'; import {reactive} from "vue"; export const useFileDataStore = defineStore('fileData', () => { - const receiveData = reactive(JSON.parse(localStorage.getItem('receiveData')||'') || []); // 接收的数据 - const shareData = reactive(JSON.parse(localStorage.getItem('shareData')||'') || []); // 接收的数据 + const receiveData = reactive(JSON.parse(localStorage.getItem('receiveData')||'[]') || []); // 接收的数据 + const shareData = reactive(JSON.parse(localStorage.getItem('shareData')||'[]') || []); // 接收的数据 function save() { localStorage.setItem('receiveData', JSON.stringify(receiveData)); localStorage.setItem('shareData', JSON.stringify(shareData)); diff --git a/main.py b/main.py index cfc6a03..2453916 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,11 @@ import random from fastapi import FastAPI, UploadFile, File, Form from starlette.middleware.cors import CORSMiddleware +from tortoise.contrib.fastapi import register_tortoise + +from apps.base.models import FileCodes +from apps.base.utils import get_file_path_name, get_expire_info +from core.storage import file_storage app = FastAPI() @@ -16,10 +21,30 @@ app.add_middleware( allow_methods=["*"], allow_headers=["*"], ) +register_tortoise( + app, + generate_schemas=True, + add_exception_handlers=True, + + config={ + 'connections': { + 'default': 'sqlite://filecodebox.db' + }, + 'apps': { + 'models': { + "models": ["apps.base.models"], + 'default_connection': 'default', + } + }, + "use_tz": False, + "timezone": "Asia/Shanghai", + } + +) @app.post('/share/text/') -async def share_text(text: str = Form(...), expireValue: int = Form(...), expireStyle: str = Form(...), targetType: str = Form(...)): +async def share_text(text: str = Form(...)): return { 'code': 200, 'msg': 'success', @@ -32,12 +57,27 @@ async def share_text(text: str = Form(...), expireValue: int = Form(...), expire @app.post('/share/file/') -async def share_file(file: UploadFile = File(default=None)): +async def share_file(expire_value: int = Form(default=1, gt=0), expire_style: str = Form(default='day'), file: UploadFile = File(...)): + expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style) + path, suffix, prefix, uuid_file_name, file_uuid, save_path = await get_file_path_name(file) + await file_storage.save_file(file, save_path) + file_code = await FileCodes.create( + code=code, + prefix=prefix, + suffix=suffix, + uuid_file_name=uuid_file_name, + file_path=path, + size=file.size, + expired_at=expired_at, + expired_count=expired_count, + used_count=used_count, + ) + await file_storage.delete_file(file_code) return { 'code': 200, 'msg': 'success', 'data': { - 'code': random.randint(100000, 999999), + 'code': code, 'text': '/share/{}'.format(random.randint(100000, 999999)), 'name': file.filename, }