From 7fc733d2254ecb28d9f3e19d29e4f0abae9678b3 Mon Sep 17 00:00:00 2001 From: lan Date: Tue, 28 Feb 2023 17:22:49 +0800 Subject: [PATCH] =?UTF-8?q?update:=E5=88=87=E7=89=87=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/storage.py | 160 ++++++++++++++++++++++++++----------------- main.py | 22 +++++- templates/index.html | 64 +++++++++++++---- 3 files changed, 166 insertions(+), 80 deletions(-) diff --git a/core/storage.py b/core/storage.py index 51adbd6..58936b1 100644 --- a/core/storage.py +++ b/core/storage.py @@ -1,5 +1,7 @@ import os import asyncio +import time +import uuid from datetime import datetime from pathlib import Path from typing import BinaryIO @@ -16,6 +18,101 @@ if settings.STORAGE_ENGINE == 'aliyunsystem': import oss2 +class FileSystemStorage: + def __init__(self): + self.DATA_ROOT = Path(settings.DATA_ROOT) + self.STATIC_URL = settings.STATIC_URL + self.NAME = "filesystem" + self.DOWN_PATH = '/select' + + async def get_filepath(self, text: str): + return self.DATA_ROOT / text.lstrip(self.STATIC_URL + '/') + + async def get_url(self, info: Codes): + return f'{self.DOWN_PATH}?code={info.code}' + + async def get_text(self, file: UploadFile, key: str): + ext = file.filename.split('.')[-1] + now = datetime.now() + path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/" + if not path.exists(): + path.mkdir(parents=True) + text = f"{self.STATIC_URL}/{(path / f'{key}.{ext}').relative_to(self.DATA_ROOT)}" + return text + + @staticmethod + async def get_size(file: UploadFile): + f = file.file + f.seek(0, os.SEEK_END) + size = f.tell() + f.seek(0, os.SEEK_SET) + return size + + @staticmethod + def _save(filepath, file: BinaryIO): + with open(filepath, 'wb') as f: + chunk_size = 256 * 1024 + chunk = file.read(chunk_size) + while chunk: + f.write(chunk) + chunk = file.read(chunk_size) + + @staticmethod + def _save_chunk(filepath, file: bytes): + with open(filepath, 'wb') as f: + f.write(file) + + async def create_upload_file(self): + file_key = uuid.uuid4().hex + file_path = self.DATA_ROOT / f"temp/{file_key}" + if not file_path.exists(): + file_path.mkdir(parents=True) + return file_key + + async def save_chunk_file(self, file_key, file_chunk, chunk_index, chunk_total): + file_path = self.DATA_ROOT / f"temp/{file_key}/" + await asyncio.to_thread(self._save_chunk, file_path / f"{chunk_total}-{chunk_index}.temp", file_chunk) + + async def merge_chunks(self, file_key, file_name, total_chunks: int): + ext = file_name.split('.')[-1] + now = datetime.now() + path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/" + if not path.exists(): + path.mkdir(parents=True) + text = f"{self.STATIC_URL}/{(path / f'{file_key}.{ext}').relative_to(self.DATA_ROOT)}" + with open(path / f'{file_key}.{ext}', 'wb') as f: + for i in range(1, total_chunks + 1): + now_temp = self.DATA_ROOT / f'temp/{file_key}/{total_chunks}-{i}.temp' + with open(now_temp, 'rb') as r: + f.write(r.read()) + await asyncio.to_thread(os.remove, now_temp) + await asyncio.to_thread(os.rmdir, self.DATA_ROOT / f'temp/{file_key}/') + return text + + async def save_file(self, file: UploadFile, text: str): + filepath = await self.get_filepath(text) + await asyncio.to_thread(self._save, filepath, file.file) + + async def delete_file(self, text: str): + filepath = await self.get_filepath(text) + if filepath.exists(): + await asyncio.to_thread(os.remove, filepath) + await asyncio.to_thread(self.judge_delete_folder, filepath) + + async def delete_files(self, texts): + tasks = [self.delete_file(text) for text in texts] + await asyncio.gather(*tasks) + + def judge_delete_folder(self, filepath): + current = filepath.parent + while current != self.DATA_ROOT: + if not list(current.iterdir()): + os.rmdir(current) + current = current.parent + else: + break + + class AliyunFileStorage: def __init__(self): auth = oss2.Auth(settings.KeyId, settings.KeySecret) @@ -71,69 +168,6 @@ class AliyunFileStorage: self.bucket.delete_object(text) -class FileSystemStorage: - def __init__(self): - self.DATA_ROOT = Path(settings.DATA_ROOT) - self.STATIC_URL = settings.STATIC_URL - self.NAME = "filesystem" - self.DOWN_PATH = '/select' - - async def get_filepath(self, text: str): - return self.DATA_ROOT / text.lstrip(self.STATIC_URL + '/') - - async def get_url(self, info: Codes): - return f'{self.DOWN_PATH}?code={info.code}' - - async def get_text(self, file: UploadFile, key: str): - ext = file.filename.split('.')[-1] - now = datetime.now() - path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/" - if not path.exists(): - path.mkdir(parents=True) - text = f"{self.STATIC_URL}/{(path / f'{key}.{ext}').relative_to(self.DATA_ROOT)}" - return text - - @staticmethod - async def get_size(file: UploadFile): - f = file.file - f.seek(0, os.SEEK_END) - size = f.tell() - f.seek(0, os.SEEK_SET) - return size - - @staticmethod - def _save(filepath, file: BinaryIO): - with open(filepath, 'wb') as f: - chunk_size = 256 * 1024 - chunk = file.read(chunk_size) - while chunk: - f.write(chunk) - chunk = file.read(chunk_size) - - async def save_file(self, file: UploadFile, text: str): - filepath = await self.get_filepath(text) - await asyncio.to_thread(self._save, filepath, file.file) - - async def delete_file(self, text: str): - filepath = await self.get_filepath(text) - if filepath.exists(): - await asyncio.to_thread(os.remove, filepath) - await asyncio.to_thread(self.judge_delete_folder, filepath) - - async def delete_files(self, texts): - tasks = [self.delete_file(text) for text in texts] - await asyncio.gather(*tasks) - - def judge_delete_folder(self, filepath): - current = filepath.parent - while current != self.DATA_ROOT: - if not list(current.iterdir()): - os.rmdir(current) - current = current.parent - else: - break - - STORAGE_ENGINE = { "filesystem": FileSystemStorage, "aliyunsystem": AliyunFileStorage diff --git a/main.py b/main.py index 6ebbefc..97ba907 100644 --- a/main.py +++ b/main.py @@ -9,12 +9,12 @@ from starlette.responses import HTMLResponse, FileResponse, RedirectResponse from starlette.staticfiles import StaticFiles from core.utils import error_ip_limit, upload_ip_limit, get_code, storage, delete_expire_files, get_token from core.depends import admin_required -from fastapi import FastAPI, Depends, UploadFile, Form, File, HTTPException, BackgroundTasks +from fastapi import FastAPI, Depends, UploadFile, Form, File, HTTPException, BackgroundTasks, Header from core.database import init_models, Options, Codes, get_session from settings import settings # 实例化FastAPI -app = FastAPI(debug=settings.DEBUG, redoc_url=None, docs_url=None, openapi_url=None) +app = FastAPI(debug=settings.DEBUG, redoc_url=None, ) @app.on_event('startup') @@ -172,6 +172,24 @@ async def get_file(code: str, token: str, ip: str = Depends(error_ip_limit), s: return FileResponse(filepath, filename=info.name) +@app.post('/file/create/') +async def create_file(): + # 生成随机字符串 + return {'code': 200, 'data': await storage.create_upload_file()} + + +@app.post('/file/upload/{file_key}/') +async def upload_file(file_key: str, file: bytes = File(...), chunk_index: int = Form(...), + total_chunks: int = Form(...)): + await storage.save_chunk_file(file_key, file, chunk_index, total_chunks) + return {'code': 200} + + +@app.get('/file/merge/{file_key}') +async def merge_chunks(file_key: str, file_name: str, total_chunks: int): + return {'code': 200, 'data': await storage.merge_chunks(file_key, file_name, total_chunks)} + + @app.post('/share', dependencies=[Depends(admin_required)], description='分享文件') async def share(background_tasks: BackgroundTasks, text: str = Form(default=None), style: str = Form(default='2'), value: int = Form(default=1), file: UploadFile = File(default=None), diff --git a/templates/index.html b/templates/index.html index e6aee18..41dd01b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -164,6 +164,7 @@ drag action="/share" multiple + :http-request="uploadFile" style="margin: 1rem 0;" :data="uploadData" :before-upload="beforeUpload" @@ -183,7 +184,6 @@ placeholder="请输入内容,使用按钮存入" v-model="uploadData.text"> -
@@ -253,7 +253,7 @@ -
+
FileCodeBox V1.6 Beta
@@ -346,19 +346,51 @@ data: data, ...config }).then(res => { - this.$message({ - message: res.data.detail, - type: 'success' - }); + if (res.data.detail) { + this.$message({ + message: res.data.detail, + type: 'success' + }); + } resolve(res.data) }).catch(err => { - this.$message({ - message: err.response.data.detail, - type: 'error' - }); + if (this.data.detail) { + this.$message({ + message: err.response.data.detail, + type: 'error' + }); + } }) }) }, + uploadChunk: async function (chunk, file_key, chunk_index, total_chunks) { + const formData = new FormData(); + formData.append('file', chunk); + formData.append('file_key', file_key); + formData.append('chunk_index', chunk_index); + formData.append('total_chunks', total_chunks); + await this.http('post', `/file/upload/${file_key}/`, formData) + }, + uploadFile: async function (e) { + this.http('post', '/file/create/').then(async res => { + const file = e.file; + let chunk_index = 0; + const shardSize = 1024 * 1024 * 5; + const {name, size} = file; + const total_chunks = Math.ceil(size / shardSize); + while (chunk_index < total_chunks) { + const start = chunk_index * shardSize + const end = Math.min(start + shardSize, size) + chunk_index += 1; + await this.uploadChunk(file.slice(start, end), res.data, chunk_index, total_chunks); + } + this.http('get', `/file/merge/${res.data}/?file_name=${file.name}&total_chunks=${total_chunks}`).then(res => { + console.log(res) + }) + console.log(chunk_index, name, size, total_chunks); + }) + + }, copyText: function (value, style = 1) { if (style === 0) { value = `我给你分享了文件,取件码:${value},${window.location.href}?code=${value}`; @@ -439,11 +471,13 @@ } }, successUpload(response, file, fileList) { - this.$message({ - message: response.detail, - type: 'success' - }); - this.jiFiles.unshift(response.data); + if (response) { + this.$message({ + message: response.detail, + type: 'success' + }); + this.jiFiles.unshift(response.data); + } }, errorUpload(error, file, fileList) { error = JSON.parse(error.message)