@@ -80,7 +80,9 @@ docs/_build/
|
|||||||
# PyBuilder
|
# PyBuilder
|
||||||
.pybuilder/
|
.pybuilder/
|
||||||
target/
|
target/
|
||||||
|
*.db
|
||||||
|
./filecodebox.db-shm
|
||||||
|
./filecodebox.db-wal
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# @Time : 2023/8/13 20:43
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : __init__.py.py
|
||||||
|
# @Software: PyCharm
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# @Time : 2023/8/14 14:38
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : __init__.py.py
|
||||||
|
# @Software: PyCharm
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# @Time : 2023/8/14 14:38
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : views.py
|
||||||
|
# @Software: PyCharm
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
admin_api = APIRouter(
|
||||||
|
prefix='/admin',
|
||||||
|
tags=['管理'],
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# @Time : 2023/8/13 20:43
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : __init__.py.py
|
||||||
|
# @Software: PyCharm
|
||||||
@@ -1,20 +1,17 @@
|
|||||||
|
# @Time : 2023/8/14 12:20
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : depends.py
|
||||||
|
# @Software: PyCharm
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from fastapi import Header, HTTPException, Request
|
from fastapi import Header, HTTPException, Request
|
||||||
|
|
||||||
from settings import settings
|
from core.response import APIResponse
|
||||||
|
|
||||||
|
|
||||||
async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None):
|
async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None):
|
||||||
if 'share' in request.url.path:
|
return False
|
||||||
if pwd != settings.ADMIN_PASSWORD and not settings.ENABLE_UPLOAD:
|
|
||||||
raise HTTPException(status_code=403, detail='本站上传功能已关闭,仅管理员可用')
|
|
||||||
else:
|
|
||||||
if settings.ADMIN_PASSWORD is None:
|
|
||||||
raise HTTPException(status_code=404, detail='您未设置管理员密码,无法使用此功能,请更新配置文件后,重启系统')
|
|
||||||
if not pwd or pwd != settings.ADMIN_PASSWORD:
|
|
||||||
raise HTTPException(status_code=401, detail="密码错误,请重新登录")
|
|
||||||
|
|
||||||
|
|
||||||
class IPRateLimit:
|
class IPRateLimit:
|
||||||
@@ -48,5 +45,5 @@ class IPRateLimit:
|
|||||||
def __call__(self, request: Request):
|
def __call__(self, request: Request):
|
||||||
ip = request.headers.get('X-Real-IP', request.headers.get('X-Forwarded-For', request.client.host))
|
ip = request.headers.get('X-Real-IP', request.headers.get('X-Forwarded-For', request.client.host))
|
||||||
if not self.check_ip(ip):
|
if not self.check_ip(ip):
|
||||||
raise HTTPException(status_code=400, detail=f"请求次数过多,请稍后再试")
|
raise HTTPException(status_code=423, detail=f"请求次数过多,请稍后再试")
|
||||||
return ip
|
return ip
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# @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
|
||||||
|
|
||||||
|
from core.utils import get_now
|
||||||
|
|
||||||
|
|
||||||
|
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文件名', null=True)
|
||||||
|
file_path: Optional[str] = fields.CharField(max_length=255, description='文件路径', null=True)
|
||||||
|
size: Optional[int] = fields.IntField(description='文件大小', default=0)
|
||||||
|
text: Optional[str] = fields.TextField(description='文本内容', null=True)
|
||||||
|
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 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
|
||||||
|
|
||||||
|
async def get_file_path(self):
|
||||||
|
return f"{self.file_path}/{self.uuid_file_name}"
|
||||||
|
|
||||||
|
|
||||||
|
file_codes_pydantic = pydantic_model_creator(FileCodes, name='FileCodes')
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class SelectFileModel(BaseModel):
|
||||||
|
code: str
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# @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.depends import IPRateLimit
|
||||||
|
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, 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
|
||||||
|
|
||||||
|
|
||||||
|
# 错误IP限制器
|
||||||
|
error_ip_limit = IPRateLimit(1, 1)
|
||||||
|
# 上传文件限制器
|
||||||
|
upload_ip_limit = IPRateLimit(10, 1)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# @Time : 2023/8/14 03:59
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : views.py
|
||||||
|
# @Software: PyCharm
|
||||||
|
from fastapi import APIRouter, Form, UploadFile, File, Depends
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
|
from apps.base.models import FileCodes
|
||||||
|
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(
|
||||||
|
prefix='/share',
|
||||||
|
tags=['分享'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@share_api.post('/text/')
|
||||||
|
async def share_text(text: str = Form(...), expire_value: int = Form(default=1, gt=0), expire_style: str = Form(default='day')):
|
||||||
|
expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style)
|
||||||
|
await FileCodes.create(
|
||||||
|
code=code,
|
||||||
|
text=text,
|
||||||
|
expired_at=expired_at,
|
||||||
|
expired_count=expired_count,
|
||||||
|
used_count=used_count,
|
||||||
|
size=len(text),
|
||||||
|
prefix='文本分享'
|
||||||
|
)
|
||||||
|
return APIResponse(detail={
|
||||||
|
'code': code,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@share_api.post('/file/')
|
||||||
|
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, save_path = await get_file_path_name(file)
|
||||||
|
await file_storage.save_file(file, save_path)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
return APIResponse(detail={
|
||||||
|
'code': code,
|
||||||
|
'name': file.filename,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@share_api.post('/select/')
|
||||||
|
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:
|
||||||
|
error_ip_limit.add_ip(ip)
|
||||||
|
return APIResponse(code=404, detail='文件不存在')
|
||||||
|
if await file_code.is_expired():
|
||||||
|
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': file_code.text if file_code.text is not None else await file_storage.get_file_url(file_code),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@share_api.get('/download')
|
||||||
|
async def download_file(key: str, code: str, ip: str = Depends(error_ip_limit)):
|
||||||
|
is_valid = await file_storage.get_select_token(code) == key
|
||||||
|
if not is_valid:
|
||||||
|
error_ip_limit.add_ip(ip)
|
||||||
|
file_code = await FileCodes.filter(code=code).first()
|
||||||
|
if not file_code:
|
||||||
|
return APIResponse(code=404, detail='文件不存在')
|
||||||
|
if file_code.text:
|
||||||
|
return APIResponse(detail=file_code.text)
|
||||||
|
else:
|
||||||
|
file_path = file_storage.root_path / await file_code.get_file_path()
|
||||||
|
if not file_path.exists():
|
||||||
|
return APIResponse(code=404, detail='文件已过期删除')
|
||||||
|
return FileResponse(file_path, filename=file_code.prefix + file_code.suffix)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# @Time : 2023/8/11 20:06
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : __init__.py.py
|
||||||
|
# @Software: PyCharm
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import Boolean, Column, Integer, String, DateTime, JSON, Text, select, insert, delete
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
|
||||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
|
||||||
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
engine = create_async_engine(settings.DATABASE_URL)
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
|
|
||||||
class Options(Base):
|
|
||||||
__tablename__ = 'options'
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
key = Column(String, unique=True, index=True)
|
|
||||||
value = Column(JSON)
|
|
||||||
|
|
||||||
|
|
||||||
class Codes(Base):
|
|
||||||
__tablename__ = "codes"
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
code = Column(String(10), unique=True, index=True)
|
|
||||||
key = Column(String(30), unique=True)
|
|
||||||
name = Column(String(500))
|
|
||||||
size = Column(Integer)
|
|
||||||
type = Column(String(20))
|
|
||||||
text = Column(Text)
|
|
||||||
used = Column(Boolean, default=False)
|
|
||||||
count = Column(Integer, default=-1)
|
|
||||||
use_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
|
|
||||||
exp_time = Column(DateTime, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def init_models(s):
|
|
||||||
async with engine.begin() as conn:
|
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
|
||||||
if await conn.scalar(select(Options).filter(Options.key == 'INSTALL')) is None:
|
|
||||||
# 如果没有存在install,则清空表,并插入默认数据
|
|
||||||
await conn.execute(delete(table=Options))
|
|
||||||
await conn.execute(insert(table=Options, values=[
|
|
||||||
{'key': 'INSTALL', 'value': settings.VERSION},
|
|
||||||
{'key': 'DEBUG', 'value': settings.DEBUG},
|
|
||||||
{'key': 'DATABASE_FILE', 'value': settings.DATABASE_FILE},
|
|
||||||
{'key': 'PORT', 'value': settings.PORT},
|
|
||||||
{'key': 'DATA_ROOT', 'value': settings.DATA_ROOT},
|
|
||||||
{'key': 'STATIC_URL', 'value': settings.STATIC_URL},
|
|
||||||
{'key': 'BANNERS', 'value': settings.BANNERS},
|
|
||||||
{'key': 'ENABLE_UPLOAD', 'value': settings.ENABLE_UPLOAD},
|
|
||||||
{'key': 'MAX_DAYS', 'value': settings.MAX_DAYS},
|
|
||||||
{'key': 'ERROR_COUNT', 'value': settings.ERROR_COUNT},
|
|
||||||
{'key': 'ERROR_MINUTE', 'value': settings.ERROR_COUNT},
|
|
||||||
{'key': 'UPLOAD_COUNT', 'value': settings.UPLOAD_COUNT},
|
|
||||||
{'key': 'UPLOAD_MINUTE', 'value': settings.UPLOAD_MINUTE},
|
|
||||||
{'key': 'DELETE_EXPIRE_FILES_INTERVAL', 'value': settings.DELETE_EXPIRE_FILES_INTERVAL},
|
|
||||||
{'key': 'ADMIN_ADDRESS', 'value': settings.ADMIN_ADDRESS},
|
|
||||||
{'key': 'ADMIN_PASSWORD', 'value': settings.ADMIN_PASSWORD},
|
|
||||||
{'key': 'FILE_SIZE_LIMIT', 'value': settings.FILE_SIZE_LIMIT},
|
|
||||||
{'key': 'TITLE', 'value': settings.TITLE},
|
|
||||||
{'key': 'DESCRIPTION', 'value': settings.DESCRIPTION},
|
|
||||||
{'key': 'KEYWORDS', 'value': settings.KEYWORDS},
|
|
||||||
{'key': 'STORAGE_ENGINE', 'value': settings.STORAGE_ENGINE},
|
|
||||||
{'key': 'STORAGE_CONFIG', 'value': {}},
|
|
||||||
]))
|
|
||||||
print(
|
|
||||||
f'初始化数据库成功!\n'
|
|
||||||
f'如您未配置.env文件,将为您随机生成信息\n'
|
|
||||||
f'您的后台地址为:/{settings.ADMIN_ADDRESS}\n'
|
|
||||||
f'您的管理员密码为:{settings.ADMIN_PASSWORD}\n'
|
|
||||||
f'请尽快修改后台信息!\n'
|
|
||||||
f'FileCodeBox https://github.com/vastsa/FileCodeBox'
|
|
||||||
)
|
|
||||||
await settings.updates(await conn.execute(select(Options).filter()))
|
|
||||||
|
|
||||||
|
|
||||||
async def get_config(key):
|
|
||||||
async with engine.begin() as conn:
|
|
||||||
return await conn.scalar(select(Options.value).filter(Options.key == key))
|
|
||||||
|
|
||||||
|
|
||||||
async def get_session():
|
|
||||||
async with AsyncSession(engine, expire_on_commit=False) as s:
|
|
||||||
yield s
|
|
||||||
@@ -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
|
||||||
@@ -1,174 +1,73 @@
|
|||||||
import os
|
# @Time : 2023/8/11 20:06
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : storage.py
|
||||||
|
# @Software: PyCharm
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import time
|
import time
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
import aioboto3
|
||||||
from pathlib import Path
|
|
||||||
from typing import BinaryIO
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from core.database import Codes
|
from apps.base.models import FileCodes
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
if settings.STORAGE_ENGINE == 'aliyunsystem':
|
|
||||||
try:
|
|
||||||
import oss2
|
|
||||||
except ImportError:
|
|
||||||
os.system('pip install oss2')
|
|
||||||
import oss2
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystemStorage:
|
class SystemFileStorage:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.DATA_ROOT = Path(settings.DATA_ROOT)
|
self.chunk_size = 256 * 1024
|
||||||
self.STATIC_URL = settings.STATIC_URL
|
self.root_path = Path('./data')
|
||||||
self.NAME = "filesystem"
|
self.token = '123456'
|
||||||
self.DOWN_PATH = '/select'
|
|
||||||
|
|
||||||
async def get_filepath(self, text: str):
|
def _save(self, file, save_path):
|
||||||
return self.DATA_ROOT / text.lstrip(self.STATIC_URL + '/')
|
with open(save_path, 'wb') as f:
|
||||||
|
chunk = file.read(self.chunk_size)
|
||||||
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:
|
while chunk:
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
chunk = file.read(chunk_size)
|
chunk = file.read(self.chunk_size)
|
||||||
|
|
||||||
@staticmethod
|
async def save_file(self, file: UploadFile, save_path: str):
|
||||||
def _save_chunk(filepath, file: bytes):
|
save_path = self.root_path / save_path
|
||||||
with open(filepath, 'wb') as f:
|
if not save_path.parent.exists():
|
||||||
f.write(file)
|
save_path.parent.mkdir(parents=True)
|
||||||
|
await asyncio.to_thread(self._save, file.file, save_path)
|
||||||
|
|
||||||
async def create_upload_file(self):
|
async def delete_file(self, file_code: FileCodes):
|
||||||
file_key = uuid.uuid4().hex
|
save_path = self.root_path / await file_code.get_file_path()
|
||||||
file_path = self.DATA_ROOT / f"temp/{file_key}"
|
if save_path.exists():
|
||||||
if not file_path.exists():
|
save_path.unlink()
|
||||||
file_path.mkdir(parents=True)
|
|
||||||
return file_key
|
|
||||||
|
|
||||||
async def save_chunk_file(self, file_key, file_chunk, chunk_index, chunk_total):
|
async def get_select_token(self, code):
|
||||||
file_path = self.DATA_ROOT / f"temp/{file_key}/"
|
return hashlib.sha256(f"{code}{int(time.time() / 1000)}000{self.token}".encode()).hexdigest()
|
||||||
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):
|
async def get_file_url(self, file_code: FileCodes):
|
||||||
ext = file_name.split('.')[-1]
|
return f'/share/download?key={await self.get_select_token(file_code.code)}&code={file_code.code}'
|
||||||
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:
|
class S3FileStorage:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
auth = oss2.Auth(settings.KeyId, settings.KeySecret)
|
self.access_key_id = ''
|
||||||
self.bucket = oss2.Bucket(auth, settings.OSS_ENDPOINT, settings.BUCKET_NAME)
|
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
|
||||||
|
)
|
||||||
|
|
||||||
def upload_file(self, upload_filepath, remote_filepath):
|
async def save_file(self, file: UploadFile, save_path: str):
|
||||||
self.bucket.put_object_from_file(remote_filepath, upload_filepath)
|
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 get_text(self, file: UploadFile, key: str):
|
async def delete_file(self, file_code: FileCodes):
|
||||||
ext = file.filename.split('.')[-1]
|
async with self.session.client("s3", endpoint_url=self.endpoint_url) as s3:
|
||||||
now = datetime.now()
|
await s3.delete_object(Bucket=self.bucket_name, Key=await file_code.get_file_path())
|
||||||
path = f"FileCodeBox/upload/{now.year}/{now.month}/{now.day}"
|
|
||||||
text = f"{path}/{f'{key}.{ext}'}"
|
|
||||||
return f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/{text}"
|
|
||||||
|
|
||||||
async def get_url(self, info: Codes):
|
async def get_file_url(self, file_code: FileCodes):
|
||||||
text = info.text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/")
|
if file_code.prefix == '文本分享':
|
||||||
url = self.bucket.sign_url('GET', text, settings.ACCESSTIME, slash_safe=True)
|
return file_code.text
|
||||||
return url
|
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)
|
||||||
@staticmethod
|
return result
|
||||||
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, remote_filepath: str):
|
|
||||||
now = int(datetime.now().timestamp())
|
|
||||||
upload_filepath = settings.DATA_ROOT + str(now)
|
|
||||||
await asyncio.to_thread(self._save, upload_filepath, file.file)
|
|
||||||
self.upload_file(upload_filepath, remote_filepath)
|
|
||||||
remote_filepath = remote_filepath.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/")
|
|
||||||
self.upload_file(upload_filepath, remote_filepath)
|
|
||||||
await asyncio.to_thread(os.remove, upload_filepath)
|
|
||||||
|
|
||||||
async def delete_files(self, texts):
|
|
||||||
tasks = [self.delete_file(text) for text in texts]
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def delete_file(self, text: str):
|
|
||||||
text = text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/")
|
|
||||||
self.bucket.delete_object(text)
|
|
||||||
|
|
||||||
|
|
||||||
STORAGE_ENGINE = {
|
file_storage = SystemFileStorage()
|
||||||
"filesystem": FileSystemStorage,
|
|
||||||
"aliyunsystem": AliyunFileStorage
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,83 +1,38 @@
|
|||||||
|
# @Time : 2023/8/13 19:54
|
||||||
|
# @Author : Lan
|
||||||
|
# @File : utils.py
|
||||||
|
# @Software: PyCharm
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
import random
|
import random
|
||||||
import asyncio
|
|
||||||
import string
|
import string
|
||||||
import time
|
|
||||||
|
|
||||||
from sqlalchemy import or_, select, delete
|
from apps.base.depends import IPRateLimit
|
||||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
|
||||||
from .database import Codes, engine
|
|
||||||
from .depends import IPRateLimit
|
|
||||||
from .storage import STORAGE_ENGINE
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
storage = STORAGE_ENGINE[settings.STORAGE_ENGINE]()
|
|
||||||
|
|
||||||
# 错误IP限制器
|
|
||||||
error_ip_limit = IPRateLimit(settings.ERROR_COUNT, settings.ERROR_MINUTE)
|
|
||||||
# 上传文件限制器
|
|
||||||
upload_ip_limit = IPRateLimit(settings.UPLOAD_COUNT, settings.UPLOAD_MINUTE)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_expire_files():
|
|
||||||
while True:
|
|
||||||
async with AsyncSession(engine, expire_on_commit=False) as s:
|
|
||||||
await error_ip_limit.remove_expired_ip()
|
|
||||||
await upload_ip_limit.remove_expired_ip()
|
|
||||||
query = select(Codes).where(or_(Codes.exp_time < datetime.datetime.now(), Codes.count == 0))
|
|
||||||
exps = (await s.execute(query)).scalars().all()
|
|
||||||
files = []
|
|
||||||
exps_ids = []
|
|
||||||
for exp in exps:
|
|
||||||
if exp.type != "text":
|
|
||||||
files.append(exp.text)
|
|
||||||
exps_ids.append(exp.id)
|
|
||||||
await storage.delete_files(files)
|
|
||||||
query = delete(Codes).where(Codes.id.in_(exps_ids))
|
|
||||||
await s.execute(query)
|
|
||||||
await s.commit()
|
|
||||||
await asyncio.sleep(settings.DELETE_EXPIRE_FILES_INTERVAL * 60)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_random_num():
|
async def get_random_num():
|
||||||
|
"""
|
||||||
|
获取随机数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return random.randint(10000, 99999)
|
return random.randint(10000, 99999)
|
||||||
|
|
||||||
|
|
||||||
|
r_s = string.ascii_uppercase + string.digits
|
||||||
|
|
||||||
|
|
||||||
async def get_random_string():
|
async def get_random_string():
|
||||||
r_s = string.ascii_letters + string.digits
|
"""
|
||||||
return ''.join(random.choice(r_s) for _ in range(5)).upper()
|
获取随机字符串
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return ''.join(random.choice(r_s) for _ in range(5))
|
||||||
|
|
||||||
|
|
||||||
async def get_code(s: AsyncSession, exp_style):
|
async def get_now():
|
||||||
if exp_style == 'forever':
|
"""
|
||||||
generate = get_random_string
|
获取当前时间
|
||||||
else:
|
:return:
|
||||||
generate = get_random_num
|
"""
|
||||||
code = await generate()
|
return datetime.datetime.now(
|
||||||
while (await s.execute(select(Codes.id).where(Codes.code == code))).scalar():
|
datetime.timezone(datetime.timedelta(hours=8))
|
||||||
code = await generate()
|
)
|
||||||
return str(code)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_token(ip, code):
|
|
||||||
return hashlib.sha256(f"{ip}{code}{int(time.time() / 1000)}000{settings.ADMIN_PASSWORD}".encode()).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
async def get_expire_info(expire_style, expire_value, s):
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
if expire_value <= 0 or expire_value > 999:
|
|
||||||
return True, None, None
|
|
||||||
code = await get_code(s, expire_style)
|
|
||||||
if expire_style == 'day':
|
|
||||||
return False, now + datetime.timedelta(days=expire_value), -1, code
|
|
||||||
elif expire_style == 'hour':
|
|
||||||
return False, now + datetime.timedelta(hours=expire_value), -1, code
|
|
||||||
elif expire_style == 'minute':
|
|
||||||
return False, now + datetime.timedelta(minutes=expire_value), -1, code
|
|
||||||
elif expire_style == 'forever':
|
|
||||||
return False, None, -1, code
|
|
||||||
elif expire_style == 'count':
|
|
||||||
return False, None, expire_value, code
|
|
||||||
else:
|
|
||||||
return True, None, None
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript',
|
||||||
|
'@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
.git
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
CardTools: typeof import('./src/components/CardTools.vue')['default']
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSubmenu: typeof import('element-plus/es')['ElSubmenu']
|
||||||
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
|
FileBox: typeof import('./src/components/FileBox.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
UploadFile: typeof import('./src/components/UploadFile.vue')['default']
|
||||||
|
UploadText: typeof import('./src/components/UploadText.vue')['default']
|
||||||
|
}
|
||||||
|
export interface ComponentCustomProperties {
|
||||||
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/logo_small.png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FileCodeBox</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "fcb-fronted",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check build-only",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"element-plus": "^2.3.9",
|
||||||
|
"pinia": "^2.1.4",
|
||||||
|
"qrcode.vue": "^3.4.1",
|
||||||
|
"sass": "^1.64.2",
|
||||||
|
"unplugin-auto-import": "^0.16.6",
|
||||||
|
"unplugin-vue-components": "^0.25.1",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
|
"@tsconfig/node18": "^18.2.0",
|
||||||
|
"@types/node": "^18.17.0",
|
||||||
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
|
"@vue/tsconfig": "^0.4.0",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"typescript": "~5.1.6",
|
||||||
|
"vite": "^4.4.6",
|
||||||
|
"vue-tsc": "^1.8.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f5f5f5;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #212121;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
html{
|
||||||
|
background: var(--vt-c-white-soft);
|
||||||
|
}
|
||||||
|
html.dark{
|
||||||
|
background: var(--vt-c-black-soft);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
color: var(--color-text);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family: Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,55 @@
|
|||||||
|
@import './base.css';
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 400px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #F8FBFE;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||||
|
border-radius: 20px !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
z-index: 999;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.box {
|
||||||
|
display: inline-block;
|
||||||
|
align-items: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.circle {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
padding: 3px;
|
||||||
|
background-color: #ff605c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow {
|
||||||
|
padding: 3px;
|
||||||
|
background-color: #ffbd44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green {
|
||||||
|
background-color: #00ca4e;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const isDark = useDark()
|
||||||
|
const toggleDark = useToggle(isDark)
|
||||||
|
import { Moon, Sunny, Upload, Back, TakeawayBox } from "@element-plus/icons-vue";
|
||||||
|
import FileBox from "@/components/FileBox.vue";
|
||||||
|
import { useFileBoxStore } from "@/stores/fileBox";
|
||||||
|
const fileBoxStore = useFileBoxStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tools">
|
||||||
|
<div class="circle">
|
||||||
|
<el-icon size="17" color="#212121" class="red box" @click="router.push({'name':route.name=='home'?'send':'home'})">
|
||||||
|
<Back v-if="route.name=='send'"></Back>
|
||||||
|
<Upload v-else></Upload>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="circle">
|
||||||
|
<el-icon size="17" color="#212121" class="yellow box" @click="toggleDark(!isDark)">
|
||||||
|
<Moon v-if="isDark"></Moon>
|
||||||
|
<Sunny v-else></Sunny>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="circle">
|
||||||
|
<el-icon size="17" color="#212121" class="green box" @click="fileBoxStore.showFileBox=true">
|
||||||
|
<TakeawayBox></TakeawayBox>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<file-box></file-box>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useFileDataStore } from "@/stores/fileData";
|
||||||
|
import { useFileBoxStore } from "@/stores/fileBox";
|
||||||
|
|
||||||
|
const fileStore = useFileDataStore();
|
||||||
|
const fileBoxStore = useFileBoxStore();
|
||||||
|
import QrcodeVue from "qrcode.vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { ref } from "vue";
|
||||||
|
const openUrl = (url: string) => {
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
url = window.location.origin + url;
|
||||||
|
}
|
||||||
|
window.open(url);
|
||||||
|
};
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const copyText = (text: any, style = 0) => {
|
||||||
|
if (style === 1) {
|
||||||
|
text = window.location.origin + '/#/?code=' + text;
|
||||||
|
}
|
||||||
|
const temp: any = document.createElement('textarea');
|
||||||
|
temp.value = text;
|
||||||
|
document.body.appendChild(temp);
|
||||||
|
temp.select();
|
||||||
|
if (document.execCommand('copy')) {
|
||||||
|
document.execCommand('copy');
|
||||||
|
}
|
||||||
|
document.body.removeChild(temp);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-drawer :append-to-body="true" v-model="fileBoxStore.showFileBox" direction="btt" style="max-width: 1080px;margin: auto;"
|
||||||
|
size="400">
|
||||||
|
<template #header>
|
||||||
|
<h4>文件箱</h4>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div v-if="route.name=='home'" style="display: flex;flex-wrap: wrap;justify-content: center">
|
||||||
|
<el-card v-for="(value,index) in fileStore.receiveData" :key="index" style="margin: 0.5rem">
|
||||||
|
<template #header>
|
||||||
|
<div style="display: flex;justify-content: space-between">
|
||||||
|
<h4 style="width: 6rem;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">{{ value.name }}</h4>
|
||||||
|
<el-button size="small" type="danger" @click="fileStore.deleteReceiveData(index)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div style="width: 200px;">
|
||||||
|
<div style="display: flex;justify-content: space-between">
|
||||||
|
<qrcode-vue :value="value.text" :size="100"></qrcode-vue>
|
||||||
|
<div style="display: flex;flex-direction: column;justify-content: space-around">
|
||||||
|
<el-tag size="large" style="cursor: pointer" @click="copyText(value.code)">{{ value.code }}</el-tag>
|
||||||
|
<el-tag v-if="value.name!=='文本分享'" size="large" type="success" style="cursor: pointer" @click="openUrl(value.text);">
|
||||||
|
点击下载
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else size="large" type="success" style="cursor: pointer" @click="copyText(value.text);">点击复制</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
<div v-else style="display: flex;flex-wrap: wrap;justify-content: center">
|
||||||
|
<el-card v-for="(value,index) in fileStore.shareData" :key="index" style="margin: 0.5rem">
|
||||||
|
<template #header>
|
||||||
|
<div style="display: flex;justify-content: space-between">
|
||||||
|
<h4 style="width: 6rem;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">{{ value.name }}</h4>
|
||||||
|
<el-button size="small" type="danger" @click="fileStore.deleteShareData(index)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div style="width: 200px;">
|
||||||
|
<el-progress v-if="value.status!='success'" striped :percentage="value.percentage" :text-inside="true"
|
||||||
|
:stroke-width="20"></el-progress>
|
||||||
|
<div style="display: flex;justify-content: space-between">
|
||||||
|
<qrcode-vue :value="value.text" :size="100"></qrcode-vue>
|
||||||
|
<div style="display: flex;flex-direction: column;justify-content: space-around">
|
||||||
|
<el-tag size="large" style="cursor: pointer" @click="copyText(value.code)">{{ value.code }}</el-tag>
|
||||||
|
<el-tag size="large" type="success" style="cursor: pointer" @click="copyText(value.code,1);">复制链接
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { request } from "@/utils/request";
|
||||||
|
import { useFileDataStore } from "@/stores/fileData";
|
||||||
|
import { useFileBoxStore } from "@/stores/fileBox";
|
||||||
|
const fileBoxStore = useFileBoxStore();
|
||||||
|
const fileStore = useFileDataStore();
|
||||||
|
const props = defineProps({
|
||||||
|
shareData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
expire_value: 1,
|
||||||
|
expire_style: 'day',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileList: any = ref([])
|
||||||
|
const uploadBox: any = ref(null)
|
||||||
|
const handleOnChangeFileList = (file: any) => {
|
||||||
|
fileStore.addShareData({
|
||||||
|
'name': file.name,
|
||||||
|
'text': '',
|
||||||
|
'status': file.status,
|
||||||
|
'percentage': 0,
|
||||||
|
'size': file.size,
|
||||||
|
'type': file.raw.type,
|
||||||
|
'uid': file.uid,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHttpRequest = (options: any) => {
|
||||||
|
fileBoxStore.showFileBox = true;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', options.file);
|
||||||
|
formData.append('expire_value', props.shareData.expireValue);
|
||||||
|
formData.append('expire_style', props.shareData.expireStyle);
|
||||||
|
request(
|
||||||
|
{
|
||||||
|
url: "share/file/",
|
||||||
|
method: "post",
|
||||||
|
data: formData,
|
||||||
|
onUploadProgress: (event: any) => {
|
||||||
|
const percentage = Math.round((event.loaded * 100) / event.total) || 0;
|
||||||
|
fileStore.shareData.forEach((file: any) => {
|
||||||
|
if (file.uid === options.file.uid) {
|
||||||
|
file.percentage = percentage;
|
||||||
|
fileStore.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).then((res: any) => {
|
||||||
|
const data = res.detail;
|
||||||
|
fileStore.shareData.forEach((file: any) => {
|
||||||
|
if (file.uid === options.file.uid) {
|
||||||
|
file.status = 'success';
|
||||||
|
file.text = data.text;
|
||||||
|
file.code = data.code;
|
||||||
|
fileStore.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: any) => {
|
||||||
|
fileStore.shareData.forEach((file: any) => {
|
||||||
|
if (file.uid === options.file.uid) {
|
||||||
|
file.status = 'fail';
|
||||||
|
file.text = err.message;
|
||||||
|
fileStore.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function pasteLister(event: any) {
|
||||||
|
const items = event.clipboardData && event.clipboardData.items;
|
||||||
|
if (items && items.length) {
|
||||||
|
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:any) {
|
||||||
|
console.log(str);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const file: any = items[i].getAsFile();
|
||||||
|
if (file) {
|
||||||
|
console.log(file);
|
||||||
|
const uid = Date.now();
|
||||||
|
file.uid = uid;
|
||||||
|
fileStore.addShareData({
|
||||||
|
'name': file.name,
|
||||||
|
'text': '',
|
||||||
|
'status': 'ready',
|
||||||
|
'percentage': 0,
|
||||||
|
'size': file.size,
|
||||||
|
'type': file.type,
|
||||||
|
'uid': uid,
|
||||||
|
});
|
||||||
|
handleHttpRequest({
|
||||||
|
file: file,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onUnmounted(()=>{
|
||||||
|
// 清除剪切板事件
|
||||||
|
document.removeEventListener('paste', pasteLister);
|
||||||
|
})
|
||||||
|
onMounted(()=>{
|
||||||
|
document.addEventListener('paste', pasteLister);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
multiple
|
||||||
|
:show-file-list="false"
|
||||||
|
ref="uploadBox"
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:on-change="handleOnChangeFileList"
|
||||||
|
:http-request="handleHttpRequest"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload">
|
||||||
|
<upload-filled/>
|
||||||
|
</el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文字、文件拖、粘贴到此处,或 <em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<div class="el-upload__text" style="font-size: 10px;">天数<7或限制次数(24h后删除)</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.el-upload {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload-dragger {
|
||||||
|
box-shadow: 3px 3px 0 0 rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { request } from "@/utils/request";
|
||||||
|
const shareText = ref('')
|
||||||
|
import { useFileDataStore } from "@/stores/fileData";
|
||||||
|
import { useFileBoxStore } from "@/stores/fileBox";
|
||||||
|
const fileBoxStore = useFileBoxStore();
|
||||||
|
const fileStore = useFileDataStore();
|
||||||
|
const props = defineProps({
|
||||||
|
shareData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
expire_value: 1,
|
||||||
|
expire_style: 'day',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const handleSubmitShareText = ()=>{
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="position: relative">
|
||||||
|
<el-input
|
||||||
|
placeholder="请输入您要寄出的文本"
|
||||||
|
v-model="shareText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="9"
|
||||||
|
:input-style="{'border-radius':'20px','border':'1px dashed var(--el-border-color)','box-shadow':'none'}"
|
||||||
|
>
|
||||||
|
</el-input>
|
||||||
|
<el-button @click="handleSubmitShareText" style="position: absolute;right: 0;top: 0;border-radius: 0 20px 0 20px;margin: 1px;background: rgba(255,255,255,0.2)" size="large">分享</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import './assets/main.css'
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('@/views/HomeView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/send',
|
||||||
|
name: 'send',
|
||||||
|
component: () => import('@/views/SendView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
name: 'admin',
|
||||||
|
component: () => import('@/views/AdminView.vue'),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
export const useFileBoxStore = defineStore('fileBox', () => {
|
||||||
|
const showFileBox = ref(false);
|
||||||
|
return { showFileBox };
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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')||'[]') || []); // 接收的数据
|
||||||
|
function save() {
|
||||||
|
localStorage.setItem('receiveData', JSON.stringify(receiveData));
|
||||||
|
localStorage.setItem('shareData', JSON.stringify(shareData));
|
||||||
|
}
|
||||||
|
function addReceiveData(data:any) {
|
||||||
|
receiveData.push(data);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addShareData(data:any) {
|
||||||
|
shareData.push(data);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteReceiveData(index: number) {
|
||||||
|
receiveData.splice(index, 1);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteShareData(index: number) {
|
||||||
|
shareData.splice(index, 1);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
return { receiveData, shareData, save, addShareData, addReceiveData, deleteReceiveData, deleteShareData };
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const request = instance;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<el-container style="width: 100vw;height: 100%;position: relative">
|
||||||
|
<el-aside width="200" style="height: 100%">
|
||||||
|
<el-menu>
|
||||||
|
<el-menu-item index="1">文件管理</el-menu-item>
|
||||||
|
<el-menu-item index="4">1</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-main>Main</el-main>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Upload, TakeawayBox } from '@element-plus/icons-vue';
|
||||||
|
import { onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import CardTools from "@/components/CardTools.vue";
|
||||||
|
import { useFileBoxStore } from "@/stores/fileBox";
|
||||||
|
import { useFileDataStore } from "@/stores/fileData";
|
||||||
|
|
||||||
|
import { request } from "@/utils/request";
|
||||||
|
const fileBoxStore = useFileBoxStore();
|
||||||
|
const fileStore = useFileDataStore();
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const code = ref('')
|
||||||
|
const input_status = reactive({
|
||||||
|
'readonly': false,
|
||||||
|
'loading': false,
|
||||||
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
const query_code = route.query.code as string;
|
||||||
|
if (query_code) {
|
||||||
|
code.value = query_code
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(code, (newVal) => {
|
||||||
|
if (newVal.length === 5) {
|
||||||
|
input_status.readonly = true;
|
||||||
|
input_status.loading = true;
|
||||||
|
request({
|
||||||
|
'url': '/share/select',
|
||||||
|
'method': 'POST',
|
||||||
|
'data': {
|
||||||
|
'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 = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const listenInput = (num: number) => {
|
||||||
|
if (code.value.length < 5) {
|
||||||
|
code.value += num
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<el-card class="card" style="padding-bottom: 1rem">
|
||||||
|
<CardTools/>
|
||||||
|
<el-row style="text-align: center">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-input :readonly="input_status.readonly" v-loading="input_status.loading" v-model="code" class="code-input" round autofocus clearable maxlength="5" placeholder="请输入五位取件码"/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span=8 v-for="i in 9" :key="i">
|
||||||
|
<el-button class="key-button" round @click="listenInput(i)">{{ i }}</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span=8>
|
||||||
|
<el-button @click="router.push({'name':'send'})" class="key-button" :icon="Upload" round>
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span=8>
|
||||||
|
<el-button class="key-button" round @click="listenInput(0)">0</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span=8>
|
||||||
|
<el-button class="key-button" round :icon="TakeawayBox" @click="fileBoxStore.showFileBox=true">
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
<div style="text-align: center; margin-top: 1rem;color: #606266">
|
||||||
|
<a style="text-decoration: none;color: #606266" target="_blank" href="https://github.com/vastsa/FileCodeBox">FileCodeBox V2.0 Beta</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
<style lang='scss'>
|
||||||
|
.key-button{
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
margin: 0.2rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
height: 100px;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1rem 0;
|
||||||
|
.el-input__wrapper{
|
||||||
|
border-radius: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import CardTools from "@/components/CardTools.vue";
|
||||||
|
import UploadFile from "@/components/UploadFile.vue";
|
||||||
|
import UploadText from "@/components/UploadText.vue";
|
||||||
|
const shareData = ref({
|
||||||
|
expireValue: 1,
|
||||||
|
expireStyle: 'day',
|
||||||
|
targetType: 'file',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<el-card class="card" style="padding: 1rem;position: relative" :body-style="{ padding: '0px 0px 20px 0px' }">
|
||||||
|
<card-tools/>
|
||||||
|
<div style="display: flex;margin-top: 1rem">
|
||||||
|
<div>
|
||||||
|
<el-input
|
||||||
|
v-model="shareData.expireValue"
|
||||||
|
style="width: 200px"
|
||||||
|
placeholder="请输入值"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<el-select v-model="shareData.expireStyle" placeholder="过期方式" style="width: 75px">
|
||||||
|
<el-option label="天数" value="day" />
|
||||||
|
<el-option label="小时" value="hour" />
|
||||||
|
<el-option label="分钟" value="minute" />
|
||||||
|
<el-option label="永久" value="forever" />
|
||||||
|
<el-option label="次数" value="count" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<span v-if="shareData.expireStyle=='day'">天</span>
|
||||||
|
<span v-else-if="shareData.expireStyle=='hour'">时</span>
|
||||||
|
<span v-else-if="shareData.expireStyle=='minute'">分</span>
|
||||||
|
<span v-else-if="shareData.expireStyle=='forever'">👌</span>
|
||||||
|
<span v-else-if="shareData.expireStyle=='count'">次</span>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<el-radio-group v-model="shareData.targetType" style="margin-left: 1rem;">
|
||||||
|
<el-radio label="file">文件</el-radio>
|
||||||
|
<el-radio label="text">文本</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1rem">
|
||||||
|
<upload-file :shareData="shareData" v-if="shareData.targetType=='file'"/>
|
||||||
|
<upload-text :shareData="shareData" v-else-if="shareData.targetType=='text'"/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node18/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,247 +1,50 @@
|
|||||||
import asyncio
|
# @Time : 2023/8/9 23:23
|
||||||
import datetime
|
# @Author : Lan
|
||||||
import uuid
|
# @File : main.py
|
||||||
from pathlib import Path
|
# @Software: PyCharm
|
||||||
|
from fastapi import FastAPI
|
||||||
from pydantic import BaseModel
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from sqlalchemy import select, func, update
|
from starlette.responses import HTMLResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import HTMLResponse, FileResponse, RedirectResponse
|
|
||||||
from starlette.staticfiles import StaticFiles
|
from starlette.staticfiles import StaticFiles
|
||||||
from core.utils import error_ip_limit, upload_ip_limit, storage, delete_expire_files, get_token, \
|
from tortoise.contrib.fastapi import register_tortoise
|
||||||
get_expire_info
|
from apps.base.views import share_api
|
||||||
from core.depends import admin_required
|
from apps.admin.views import admin_api
|
||||||
from fastapi import FastAPI, Depends, Form, File, HTTPException
|
|
||||||
from core.database import init_models, Options, Codes, get_session
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
# 实例化FastAPI
|
app = FastAPI()
|
||||||
app = FastAPI(debug=settings.DEBUG, redoc_url=None, )
|
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
@app.on_event('startup')
|
app.mount('/assets', StaticFiles(directory='./fcb-fronted/dist/assets'), name="assets")
|
||||||
async def startup(s: AsyncSession = Depends(get_session)):
|
|
||||||
# 初始化数据库
|
|
||||||
await init_models(s)
|
|
||||||
# 启动后台任务,不定时删除过期文件
|
|
||||||
asyncio.create_task(delete_expire_files())
|
|
||||||
|
|
||||||
|
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.include_router(share_api)
|
||||||
DATA_ROOT = Path(settings.DATA_ROOT)
|
app.include_router(admin_api)
|
||||||
if not DATA_ROOT.exists():
|
|
||||||
DATA_ROOT.mkdir(parents=True)
|
|
||||||
|
|
||||||
# 静态文件夹,这个固定就行了,静态资源都放在这里
|
|
||||||
app.mount('/static', StaticFiles(directory='./static'), name="static")
|
|
||||||
|
|
||||||
# 首页页面
|
|
||||||
index_html = open('templates/index.html', 'r', encoding='utf-8').read()
|
|
||||||
# 管理页面
|
|
||||||
admin_html = open('templates/admin.html', 'r', encoding='utf-8').read()
|
|
||||||
|
|
||||||
|
|
||||||
@app.get('/')
|
@app.get('/')
|
||||||
async def index():
|
async def index():
|
||||||
return HTMLResponse(
|
return HTMLResponse(content=open('./fcb-fronted/dist/index.html', 'r', encoding='utf-8').read(), status_code=200)
|
||||||
index_html
|
|
||||||
.replace('{{title}}', settings.TITLE)
|
|
||||||
.replace('{{description}}', settings.DESCRIPTION)
|
|
||||||
.replace('{{keywords}}', settings.KEYWORDS)
|
|
||||||
.replace("'{{fileSizeLimit}}'", str(settings.FILE_SIZE_LIMIT))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get(f'/{settings.ADMIN_ADDRESS}', description='管理页面')
|
|
||||||
async def admin():
|
|
||||||
return HTMLResponse(
|
|
||||||
admin_html
|
|
||||||
.replace('{{title}}', settings.TITLE)
|
|
||||||
.replace('{{description}}', settings.DESCRIPTION)
|
|
||||||
.replace('{{admin_address}}', settings.ADMIN_ADDRESS)
|
|
||||||
.replace('{{keywords}}', settings.KEYWORDS)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='查询数据库列表')
|
|
||||||
async def admin_post(page: int = Form(default=1), size: int = Form(default=10), s: AsyncSession = Depends(get_session)):
|
|
||||||
infos = (await s.execute(select(Codes).offset((page - 1) * size).limit(size))).scalars().all()
|
|
||||||
data = [{
|
|
||||||
'id': info.id,
|
|
||||||
'code': info.code,
|
|
||||||
'name': info.name,
|
|
||||||
'exp_time': info.exp_time,
|
|
||||||
'count': info.count,
|
|
||||||
'text': info.text if info.type == 'text' else await storage.get_url(info),
|
|
||||||
} for info in infos]
|
|
||||||
return {
|
|
||||||
'detail': '查询成功',
|
|
||||||
'data': data,
|
|
||||||
'paginate': {
|
|
||||||
'page': page,
|
|
||||||
'size': size,
|
|
||||||
'total': (await s.execute(select(func.count(Codes.id)))).scalar()
|
|
||||||
}}
|
|
||||||
|
|
||||||
|
|
||||||
@app.delete(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='删除数据库记录')
|
|
||||||
async def admin_delete(code: str, s: AsyncSession = Depends(get_session)):
|
|
||||||
# 找到相应记录
|
|
||||||
query = select(Codes).where(Codes.code == code)
|
|
||||||
# 找到第一条记录
|
|
||||||
file = (await s.execute(query)).scalars().first()
|
|
||||||
# 如果记录存在,并且不是文本
|
|
||||||
if file and file.type != 'text':
|
|
||||||
# 删除文件
|
|
||||||
await storage.delete_file(file.text)
|
|
||||||
# 删除数据库记录
|
|
||||||
await s.delete(file)
|
|
||||||
await s.commit()
|
|
||||||
return {'detail': '删除成功'}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get(f'/{settings.ADMIN_ADDRESS}/config', description='获取系统配置', dependencies=[Depends(admin_required)])
|
|
||||||
async def config(s: AsyncSession = Depends(get_session)):
|
|
||||||
# 查询数据库
|
|
||||||
data = {}
|
|
||||||
for i in (await s.execute(select(Options))).scalars().all():
|
|
||||||
data[i.key] = i.value
|
|
||||||
return {'detail': '获取成功', 'data': data, 'menus': [
|
|
||||||
{'key': 'INSTALL', 'name': '版本信息'},
|
|
||||||
{'key': 'WEBSITE', 'name': '网站设置'},
|
|
||||||
{'key': 'SHARE', 'name': '分享设置'},
|
|
||||||
{'key': 'BANNERS', 'name': 'Banner'},
|
|
||||||
]}
|
|
||||||
|
|
||||||
|
|
||||||
@app.patch(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends()], description='修改数据库数据')
|
|
||||||
async def admin_patch(request: Request, s: AsyncSession = Depends(get_session)):
|
|
||||||
data = await request.json()
|
|
||||||
data.pop('INSTALL')
|
|
||||||
for key, value in data.items():
|
|
||||||
await s.execute(update(Options).where(Options.key == key).values(value=value))
|
|
||||||
await settings.update(key, value)
|
|
||||||
await s.commit()
|
|
||||||
await settings.updates([[i.id, i.key, i.value] for i in (await s.execute(select(Options))).scalars().all()])
|
|
||||||
return {'detail': '修改成功'}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post('/')
|
|
||||||
async def index(code: str, ip: str = Depends(error_ip_limit), s: AsyncSession = Depends(get_session)):
|
|
||||||
query = select(Codes).where(Codes.code == code)
|
|
||||||
info = (await s.execute(query)).scalars().first()
|
|
||||||
if not info:
|
|
||||||
error_count = settings.ERROR_COUNT - error_ip_limit.add_ip(ip)
|
|
||||||
raise HTTPException(status_code=404, detail=f"取件码错误,{error_count}次后将被禁止{settings.ERROR_MINUTE}分钟")
|
|
||||||
if (info.exp_time and info.exp_time < datetime.datetime.now()) or info.count == 0:
|
|
||||||
raise HTTPException(status_code=404, detail="取件码已失效,请联系寄件人")
|
|
||||||
await s.execute(update(Codes).where(Codes.id == info.id).values(count=info.count - 1))
|
|
||||||
await s.commit()
|
|
||||||
if info.type != 'text':
|
|
||||||
info.text = f'/select?code={info.code}&token={await get_token(code, ip)}'
|
|
||||||
return {
|
|
||||||
'detail': f'取件成功,请立即下载,避免失效!',
|
|
||||||
'data': {'type': info.type, 'text': info.text, 'name': info.name, 'code': info.code}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get('/banner')
|
|
||||||
async def banner(request: Request):
|
|
||||||
return {
|
|
||||||
'detail': '查询成功',
|
|
||||||
'data': settings.BANNERS,
|
|
||||||
'enable': request.headers.get('pwd', '') == settings.ADMIN_PASSWORD or settings.ENABLE_UPLOAD,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get('/select')
|
|
||||||
async def get_file(code: str, token: str, ip: str = Depends(error_ip_limit), s: AsyncSession = Depends(get_session)):
|
|
||||||
# 验证token
|
|
||||||
if token != await get_token(code, ip):
|
|
||||||
error_ip_limit.add_ip(ip)
|
|
||||||
raise HTTPException(status_code=403, detail="口令错误,或已过期,次数过多将被禁止访问")
|
|
||||||
# 查出数据库记录
|
|
||||||
query = select(Codes).where(Codes.code == code)
|
|
||||||
info = (await s.execute(query)).scalars().first()
|
|
||||||
# 如果记录不存在,IP错误次数+1
|
|
||||||
if not info:
|
|
||||||
error_ip_limit.add_ip(ip)
|
|
||||||
raise HTTPException(status_code=404, detail="口令不存在,次数过多将被禁止访问")
|
|
||||||
# 如果是文本,直接返回
|
|
||||||
if info.type == 'text':
|
|
||||||
return {'detail': '查询成功', 'data': info.text}
|
|
||||||
# 如果是文件,返回文件
|
|
||||||
elif storage.NAME != 'filesystem':
|
|
||||||
# 重定向到文件存储服务器
|
|
||||||
return RedirectResponse(await storage.get_url(info))
|
|
||||||
else:
|
|
||||||
filepath = await storage.get_filepath(info.text)
|
|
||||||
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)}
|
|
||||||
|
|
||||||
|
|
||||||
class ShareDataModel(BaseModel):
|
|
||||||
text: str
|
|
||||||
size: int = 0
|
|
||||||
exp_style: str
|
|
||||||
exp_value: int
|
|
||||||
type: str
|
|
||||||
name: str
|
|
||||||
key: str = uuid.uuid4().hex
|
|
||||||
|
|
||||||
|
|
||||||
@app.post('/share/file/', dependencies=[Depends(admin_required)], description='分享文件')
|
|
||||||
async def share_file(file_model: ShareDataModel, s: AsyncSession = Depends(get_session),
|
|
||||||
ip: str = Depends(error_ip_limit)):
|
|
||||||
exp_error, exp_time, exp_count, code = await get_expire_info(file_model.exp_style, file_model.exp_value, s)
|
|
||||||
if exp_error:
|
|
||||||
raise HTTPException(status_code=400, detail='过期值异常')
|
|
||||||
s.add(Codes(code=code, text=file_model.text, size=file_model.size, type=file_model.type, name=file_model.name,
|
|
||||||
count=exp_count, exp_time=exp_time, key=file_model.key))
|
|
||||||
await s.commit()
|
|
||||||
upload_ip_limit.add_ip(ip)
|
|
||||||
return {
|
|
||||||
'detail': '分享成功,请点击我的文件按钮查看上传列表',
|
|
||||||
'data': {'code': code, 'key': file_model.key, 'name': file_model.name}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post('/share/text/', dependencies=[Depends(admin_required)])
|
|
||||||
async def share_text(text_model: ShareDataModel, s: AsyncSession = Depends(get_session),
|
|
||||||
ip: str = Depends(error_ip_limit)):
|
|
||||||
exp_error, exp_time, exp_count, code = await get_expire_info(text_model.exp_style, text_model.exp_value, s, )
|
|
||||||
if exp_error:
|
|
||||||
raise HTTPException(status_code=400, detail='过期值异常')
|
|
||||||
exp_status, exp_time, exp_count, code = await get_expire_info(text_model.exp_style, text_model.exp_value, s)
|
|
||||||
size, _text, _type, name, key = len(text_model.text), text_model.text, 'text', '文本分享', text_model.key
|
|
||||||
s.add(Codes(code=code, text=_text, size=size, type=_type, name=name, count=exp_count, exp_time=exp_time, key=key))
|
|
||||||
await s.commit()
|
|
||||||
upload_ip_limit.add_ip(ip)
|
|
||||||
return {
|
|
||||||
'detail': '分享成功,请点击我的文件按钮查看上传列表',
|
|
||||||
'data': {'code': code, 'key': key, 'name': name}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
uvicorn.run('main:app', host='0.0.0.0', port=settings.PORT, reload=settings.DEBUG)
|
|
||||||
|
|||||||
@@ -1,308 +0,0 @@
|
|||||||
<div align="center">
|
|
||||||
<h1>文件快递柜-轻量</h1>
|
|
||||||
<h2>FileCodeBox-Lite</h2>
|
|
||||||
<p><em>匿名口令分享文本,文件,像拿快递一样取文件</em></p>
|
|
||||||
<p>交流Q群:739673698,欢迎各位集思广益,项目构思重构中</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[简体中文](./readme.md) | [English](./readme_en.md)
|
|
||||||
|
|
||||||
## 主要特色
|
|
||||||
|
|
||||||
- [x] 轻量简洁:Fastapi+Sqlite3+Vue2+ElementUI
|
|
||||||
- [x] 轻松上传:复制粘贴,拖拽选择
|
|
||||||
- [x] 多种类型:文本,文件
|
|
||||||
- [x] 防止爆破:错误次数限制
|
|
||||||
- [x] 防止滥用:IP限制上传次数
|
|
||||||
- [x] 口令分享:随机口令,存取文件,自定义次数以及有效期
|
|
||||||
- [x] 匿名分享:无需注册,无需登录
|
|
||||||
- [x] 管理面板:查看所有文件,删除文件
|
|
||||||
- [x] 一键部署:docker一键部署
|
|
||||||
- [x] 自由拓展:阿里云OSS、本地文件流,可根据需求在storage文件中新增存储引擎
|
|
||||||
- [x] 简单明了:适合新手练手项目
|
|
||||||
|
|
||||||
## 部署方式
|
|
||||||
|
|
||||||
### Docker一键部署
|
|
||||||
|
|
||||||
#### AMD 开发版(不稳定,待测试,新增分片异步上传,永久存储,不建议使用,很多没发现的坑)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:pre
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### AMD
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ARM
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:arm
|
|
||||||
```
|
|
||||||
|
|
||||||
### 宝塔部署
|
|
||||||
|
|
||||||
https://www.yuque.com/lxyo/work/lc1oe0xqk8t9b976
|
|
||||||
|
|
||||||
### 更新方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
// 停止容器并删除
|
|
||||||
docker stop filecodebox && docker rm filecodebox
|
|
||||||
// 重新运行容器
|
|
||||||
docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.6版本注意
|
|
||||||
|
|
||||||
这一版改变比较大,如果出现问题可以尝试清空/opt/FileCodeBox目录,有问题欢迎反馈留言
|
|
||||||
注意,如果是第一次安装,请查看docker日志获取初始密码和后台地址,参考指令
|
|
||||||
后台本地文件列表,需要将服务器文件移动至目录/opt/FileCodeBox/data/locals,这样就可以显示了。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs filecodebox
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 其他方式
|
|
||||||
|
|
||||||
仅供参考,历史版本->[部署文档](https://www.yuque.com/lxyo/work/zd0kvzy7fofx6w7v)
|
|
||||||
|
|
||||||
## 项目规划
|
|
||||||
|
|
||||||
2022年12月14日
|
|
||||||
这个项目的灵感来源于丁丁快传,然后写了这么一个基于本机存储的快传系统,本系统主要是以轻量,单用户,离线环境(`私有化`
|
|
||||||
)为主,因此也不需要加太多东西,所以其实这个项目到这基本功能已经完成了,剩下的就是维护和完善现有功能。
|
|
||||||
也不会再加入新的大功能了,如果你有更好的想法和建议欢迎提issue。
|
|
||||||
|
|
||||||
## 预览
|
|
||||||
|
|
||||||
### 例站
|
|
||||||
|
|
||||||
[https://share.lanol.cn](https://share.lanol.cn)
|
|
||||||
|
|
||||||
### 暗黑模式
|
|
||||||
|
|
||||||
<table style="width:100%">
|
|
||||||
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_10.png" alt="寄文件">
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_11.png" alt="寄文件">
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### 寄件
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_1.png" alt="寄文件">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_2.png" alt="寄文本">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="width: 100%;">
|
|
||||||
<td colspan="2" style="width: 100%;">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_3.png" alt="寄文本">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### 取件
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_6.png" alt="取件">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_5.png" alt="取件码错误">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="width: 100%;">
|
|
||||||
<td colspan="2" style="width: 100%;">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_4.png" alt="取文件">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### 管理
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_7.png" alt="admin">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_12.png" alt="admin">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="width: 100%;">
|
|
||||||
<td colspan="2" style="width: 100%;">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_13.png" alt="admin">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## 配置文件
|
|
||||||
|
|
||||||
如果需要修改配置,可以将该文件放在`/opt/FileCodeBox/`目录下,并命名为`.env`,然后重启容器即可。
|
|
||||||
如果不是Docker,则需要在项目同目录下新建一个`data`文件夹,然后在创建`.env`文件
|
|
||||||
|
|
||||||
```dotenv
|
|
||||||
# 端口
|
|
||||||
PORT=12345
|
|
||||||
# Sqlite数据库文件
|
|
||||||
DATABASE_URL=sqlite+aiosqlite:///database.db
|
|
||||||
# 静态文件夹
|
|
||||||
DATA_ROOT=./static
|
|
||||||
# 静态文件夹URL
|
|
||||||
STATIC_URL=/static
|
|
||||||
# 开启上传
|
|
||||||
ENABLE_UPLOAD=True
|
|
||||||
# 错误次数
|
|
||||||
ERROR_COUNT=5
|
|
||||||
# 错误限制分钟数
|
|
||||||
ERROR_MINUTE=10
|
|
||||||
# 上传次数
|
|
||||||
UPLOAD_COUNT=60
|
|
||||||
# 上传限制分钟数
|
|
||||||
UPLOAD_MINUTE=1
|
|
||||||
# 删除过期文件的间隔(分钟)
|
|
||||||
DELETE_EXPIRE_FILES_INTERVAL=10
|
|
||||||
# 管理地址
|
|
||||||
ADMIN_ADDRESS=admin
|
|
||||||
# 管理密码
|
|
||||||
ADMIN_PASSWORD=admin
|
|
||||||
# 文件大小限制,默认10MB
|
|
||||||
FILE_SIZE_LIMIT=10
|
|
||||||
# 网站标题
|
|
||||||
TITLE=文件快递柜
|
|
||||||
# 网站描述
|
|
||||||
DESCRIPTION=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件
|
|
||||||
# 网站关键词
|
|
||||||
KEYWORDS=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件
|
|
||||||
# 存储引擎
|
|
||||||
STORAGE_ENGINE=filesystem
|
|
||||||
# 如果使用阿里云OSS服务的话需要额外创建如下参数:
|
|
||||||
# 阿里云账号AccessKey
|
|
||||||
KeyId=阿里云账号AccessKey
|
|
||||||
# 阿里云账号AccessKeySecret
|
|
||||||
KeySecret=阿里云账号AccessKeySecret
|
|
||||||
# 阿里云OSS Bucket的地域节点
|
|
||||||
OSS_ENDPOINT=阿里云OSS Bucket的地域节点
|
|
||||||
# 阿里云OSS Bucket的BucketName
|
|
||||||
BUCKET_NAME=阿里云OSS Bucket的BucketName
|
|
||||||
```
|
|
||||||
|
|
||||||
## 接口文档
|
|
||||||
|
|
||||||
前端比较简陋,可以使用接口进行二次开发
|
|
||||||
|
|
||||||
### 取件
|
|
||||||
|
|
||||||
#### PATH
|
|
||||||
|
|
||||||
`/`
|
|
||||||
|
|
||||||
#### METHOD
|
|
||||||
|
|
||||||
`POST`
|
|
||||||
|
|
||||||
#### PARAMS
|
|
||||||
|
|
||||||
code: 取件码
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"detail": "msg",
|
|
||||||
"data": {
|
|
||||||
"type": "类型",
|
|
||||||
"text": "文本",
|
|
||||||
"name": "名称",
|
|
||||||
"code": "取件码"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 寄件
|
|
||||||
|
|
||||||
#### PATH
|
|
||||||
|
|
||||||
`/share`
|
|
||||||
|
|
||||||
#### METHOD
|
|
||||||
|
|
||||||
`POST`
|
|
||||||
|
|
||||||
#### PARAMS
|
|
||||||
|
|
||||||
style: 1为次数,2为时间
|
|
||||||
value: 次数或时间
|
|
||||||
text: 取件码
|
|
||||||
file: 文件
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"detail": "msg",
|
|
||||||
"data": {
|
|
||||||
"code": "类型",
|
|
||||||
"key": "唯一ID",
|
|
||||||
"name": "名称"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 状态
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://star-history.com/#vastsa/FileCodeBox&Date)
|
|
||||||
|
|
||||||
## 赞赏
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%;text-align: center;">
|
|
||||||
支付宝
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_9.png" alt="支付宝">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%;text-align: center">
|
|
||||||
微信
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_8.png" alt="微信">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
1. 413 Request Entity Too Large
|
|
||||||
Nginx限制:
|
|
||||||
找到自己主机的nginx.conf配置文件,打开
|
|
||||||
在http{}中加入 client_max_body_size 10m;
|
|
||||||
然后重启nginx
|
|
||||||
|
|
||||||
## 免责声明
|
|
||||||
|
|
||||||
本项目开源仅供学习使用,不得用于任何违法用途,否则后果自负,与本人无关。使用请保留项目地址谢谢。
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
<div align="center">
|
|
||||||
<h1>File Express Cabinet - Lite</h1>
|
|
||||||
<h2>FileCodeBox - Lite</h2>
|
|
||||||
<p><em>share text and files with anonymous passwords, and take files like express delivery </em></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[简体中文](./readme.md) | [English](./readme_en.md)
|
|
||||||
|
|
||||||
## Main features
|
|
||||||
|
|
||||||
- [x] lightweight and simple: Fastapi + Sqlite3 + Vue2 + ElementUI
|
|
||||||
- [x] easy upload: copy and paste, drag and drop
|
|
||||||
- [x] multiple types: Text, File
|
|
||||||
- [x] explosion Prevention: error count limit
|
|
||||||
- [x] prevent abuse: IP address limits the number of uploads
|
|
||||||
- [x] password sharing: random password, file access, custom times, and validity period
|
|
||||||
- [x] anonymous sharing: no registration, no login
|
|
||||||
- [x] management Panel: View all files and delete them
|
|
||||||
- [x] one-click deployment: docker one-click deployment
|
|
||||||
- [x] A variety of storage methods : Aliyun OSS、 local file flow
|
|
||||||
|
|
||||||
## Deployment method
|
|
||||||
|
|
||||||
### One-click Docker deployment
|
|
||||||
|
|
||||||
#### AMD
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ARM
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --restart=always -p 12345:12345 -v /Users/lan/soft/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:arm
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update
|
|
||||||
|
|
||||||
```bash
|
|
||||||
// 找到容器ID
|
|
||||||
docker ps -a
|
|
||||||
// 停止容器并删除
|
|
||||||
docker stop 容器ID && docker rm 容器ID
|
|
||||||
// 重新运行容器
|
|
||||||
docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Other methods
|
|
||||||
|
|
||||||
For reference only, historical version->[部署文档](https://www.yuque.com/lxyo/work/zd0kvzy7fofx6w7v)
|
|
||||||
|
|
||||||
## Project Plan
|
|
||||||
|
|
||||||
December 14, 2022
|
|
||||||
This project is mainly light-weight, mainly single-user, offline environment, so there is no need to add too many
|
|
||||||
things, so in fact, the basic functions of this project have been completed, and the rest is to maintain and improve the
|
|
||||||
existing functions.
|
|
||||||
|
|
||||||
No new major functions will be added. If there are new functions, it will be our Pro version. Of course, it will
|
|
||||||
continue to be open source. It is an honor to be open source with @veoco. I learned from his code Many, I basically used
|
|
||||||
the Django set before, and only used Fastapi. Many of his writing methods have benefited me a lot, and I have a deeper
|
|
||||||
understanding of Fastapi, so I will also use Fastapi in the Pro version .
|
|
||||||
|
|
||||||
According to some current feedback, I hope to add multi-user functions and multi-storage engines, etc. Welcome to
|
|
||||||
continue to give comments and join us in joint development.
|
|
||||||
|
|
||||||
If you have better ideas and suggestions, welcome to file an issue.
|
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
### Demo
|
|
||||||
|
|
||||||
[https://share.lanol.cn](https://share.lanol.cn)
|
|
||||||
|
|
||||||
### Dark Theme
|
|
||||||
|
|
||||||
<table style="width:100%">
|
|
||||||
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_10.png" alt="寄文件">
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_11.png" alt="寄文件">
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### Send
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_1.png" alt="寄文件">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_2.png" alt="寄文本">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="width: 100%;">
|
|
||||||
<td colspan="2" style="width: 100%;">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_3.png" alt="寄文本">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### Receive
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_6.png" alt="取件">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_5.png" alt="取件码错误">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="width: 100%;">
|
|
||||||
<td colspan="2" style="width: 100%;">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_4.png" alt="取文件">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### Manage
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_7.png" alt="admin">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_12.png" alt="admin">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="width: 100%;">
|
|
||||||
<td colspan="2" style="width: 100%;">
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_13.png" alt="admin">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Configuration file
|
|
||||||
|
|
||||||
if you need to modify the configuration, you can put the file in `/opt/FileCodeBox/` directory and name it `.env` , and
|
|
||||||
then restart the container.
|
|
||||||
If it is not Docker, you need to create a `data` folder in the same directory as the project, and then create a `.env`
|
|
||||||
file
|
|
||||||
|
|
||||||
```dotenv
|
|
||||||
# 端口
|
|
||||||
PORT=12345
|
|
||||||
# Sqlite数据库文件
|
|
||||||
DATABASE_URL=sqlite+aiosqlite:///database.db
|
|
||||||
# 静态文件夹
|
|
||||||
DATA_ROOT=./static
|
|
||||||
# 静态文件夹URL
|
|
||||||
STATIC_URL=/static
|
|
||||||
# 开启上传
|
|
||||||
ENABLE_UPLOAD=True
|
|
||||||
# 错误次数
|
|
||||||
ERROR_COUNT=5
|
|
||||||
# 错误限制分钟数
|
|
||||||
ERROR_MINUTE=10
|
|
||||||
# 上传次数
|
|
||||||
UPLOAD_COUNT=60
|
|
||||||
# 上传限制分钟数
|
|
||||||
UPLOAD_MINUTE=1
|
|
||||||
# 删除过期文件的间隔(分钟)
|
|
||||||
DELETE_EXPIRE_FILES_INTERVAL=10
|
|
||||||
# 管理地址
|
|
||||||
ADMIN_ADDRESS=admin
|
|
||||||
# 管理密码
|
|
||||||
ADMIN_PASSWORD=admin
|
|
||||||
# 文件大小限制,默认10MB
|
|
||||||
FILE_SIZE_LIMIT=10
|
|
||||||
# 网站标题
|
|
||||||
TITLE=文件快递柜
|
|
||||||
# 网站描述
|
|
||||||
DESCRIPTION=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件
|
|
||||||
# 网站关键词
|
|
||||||
KEYWORDS=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件
|
|
||||||
# 存储引擎
|
|
||||||
STORAGE_ENGINE=filesystem
|
|
||||||
# 如果使用阿里云OSS服务的话需要额外创建如下参数:
|
|
||||||
# 阿里云账号AccessKey
|
|
||||||
KeyId=阿里云账号AccessKey
|
|
||||||
# 阿里云账号AccessKeySecret
|
|
||||||
KeySecret=阿里云账号AccessKeySecret
|
|
||||||
# 阿里云OSS Bucket的地域节点
|
|
||||||
OSS_ENDPOINT=阿里云OSS Bucket的地域节点
|
|
||||||
# 阿里云OSS Bucket的BucketName
|
|
||||||
BUCKET_NAME=阿里云OSS Bucket的BucketName
|
|
||||||
```
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://star-history.com/#vastsa/FileCodeBox&Date)
|
|
||||||
|
|
||||||
## Appreciate
|
|
||||||
|
|
||||||
<table style="width: 100%">
|
|
||||||
<tr style="width: 100%">
|
|
||||||
<td style="width: 50%;text-align: center;">
|
|
||||||
支付宝
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_9.png" alt="支付宝">
|
|
||||||
</td>
|
|
||||||
<td style="width: 50%;text-align: center">
|
|
||||||
微信
|
|
||||||
<img src="https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/images/img_8.png" alt="微信">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
this project is open source for learning only and cannot be used for any illegal purposes. Otherwise, you will be
|
|
||||||
responsible for the consequences and have nothing to do with yourself. Please keep the project address. Thank you.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
fastapi==0.88.0
|
|
||||||
aiosqlite==0.17.0
|
|
||||||
SQLAlchemy==1.4.44
|
|
||||||
python-multipart==0.0.5
|
|
||||||
uvicorn==0.15.0
|
|
||||||
greenlet==2.0.1
|
|
||||||
starlette~=0.22.0
|
|
||||||
oss2==2.16.0
|
|
||||||
uvicorn==0.15.0
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import uuid
|
|
||||||
|
|
||||||
from starlette.config import Config
|
|
||||||
|
|
||||||
# 配置文件.env,存放为data/.env
|
|
||||||
config = Config("data/.env")
|
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
# 项目版本
|
|
||||||
VERSION: str = config("VERSION", default="1.6")
|
|
||||||
# 是否开启DEBUG模式
|
|
||||||
DEBUG = config('DEBUG', cast=bool, default=False)
|
|
||||||
# 端口
|
|
||||||
PORT = config('PORT', cast=int, default=12345)
|
|
||||||
# Sqlite数据库文件
|
|
||||||
DATABASE_FILE = config('DATABASE_FILE', cast=str, default='data/database.db')
|
|
||||||
# Sqlite套接字
|
|
||||||
DATABASE_URL = config('DATABASE_URL', cast=str, default=f"sqlite+aiosqlite:///{DATABASE_FILE}")
|
|
||||||
# 数据存储文件夹,文件就不暴露在静态资源里面了
|
|
||||||
DATA_ROOT = config('DATA_ROOT', cast=str, default=f"./data/static")
|
|
||||||
# 静态文件夹URL
|
|
||||||
STATIC_URL = config('STATIC_URL', cast=str, default="/static")
|
|
||||||
# 开启上传
|
|
||||||
ENABLE_UPLOAD = config('ENABLE_UPLOAD', cast=bool, default=True)
|
|
||||||
# 最长天数
|
|
||||||
MAX_DAYS = config('MAX_DAYS', cast=int, default=7)
|
|
||||||
# 错误次数
|
|
||||||
ERROR_COUNT = config('ERROR_COUNT', cast=int, default=5)
|
|
||||||
# 错误限制分钟数
|
|
||||||
ERROR_MINUTE = config('ERROR_MINUTE', cast=int, default=10)
|
|
||||||
# 上传次数
|
|
||||||
UPLOAD_COUNT = config('UPLOAD_COUNT', cast=int, default=60)
|
|
||||||
# 是否允许永久保存
|
|
||||||
ENABLE_PERMANENT = config('ENABLE_PERMANENT', cast=bool, default=True)
|
|
||||||
# 上传限制分钟数
|
|
||||||
UPLOAD_MINUTE = config('UPLOAD_MINUTE', cast=int, default=1)
|
|
||||||
# 删除过期文件的间隔(分钟)
|
|
||||||
DELETE_EXPIRE_FILES_INTERVAL = config('DELETE_EXPIRE_FILES_INTERVAL', cast=int, default=10)
|
|
||||||
# 管理地址
|
|
||||||
ADMIN_ADDRESS = config('ADMIN_ADDRESS', cast=str, default=uuid.uuid4().hex)
|
|
||||||
# 管理密码
|
|
||||||
ADMIN_PASSWORD = config('ADMIN_PASSWORD', cast=str, default=uuid.uuid4().hex)
|
|
||||||
# 文件大小限制,默认10MB
|
|
||||||
FILE_SIZE_LIMIT = config('FILE_SIZE_LIMIT', cast=int, default=10 * 1024 * 1024)
|
|
||||||
# 网站标题
|
|
||||||
TITLE = config('TITLE', cast=str, default="文件快递柜")
|
|
||||||
# 网站描述
|
|
||||||
DESCRIPTION = config('DESCRIPTION', cast=str, default="FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件")
|
|
||||||
# 网站关键词
|
|
||||||
KEYWORDS = config('KEYWORDS', cast=str, default="FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件")
|
|
||||||
# 存储引擎:['aliyunsystem','filesystem']
|
|
||||||
STORAGE_ENGINE = config('STORAGE_ENGINE', cast=str, default="filesystem")
|
|
||||||
# 存储引擎配置
|
|
||||||
STORAGE_CONFIG = {}
|
|
||||||
# Banners
|
|
||||||
BANNERS = [{
|
|
||||||
'text': 'FileCodeBox',
|
|
||||||
'url': 'https://github.com/vastsa/FileCodeBox',
|
|
||||||
'src': '/static/banners/img_1.png'
|
|
||||||
}, {
|
|
||||||
'text': 'LanBlog',
|
|
||||||
'url': 'https://www.lanol.cn',
|
|
||||||
'src': '/static/banners/img_2.png'
|
|
||||||
}]
|
|
||||||
int_dict = {'PORT', 'MAX_DAYS', 'ERROR_COUNT', 'ERROR_MINUTE', 'UPLOAD_COUNT', 'UPLOAD_MINUTE',
|
|
||||||
'DELETE_EXPIRE_FILES_INTERVAL', 'FILE_SIZE_LIMIT'}
|
|
||||||
bool_dict = {'DEBUG', 'ENABLE_UPLOAD'}
|
|
||||||
|
|
||||||
async def update(self, key, value) -> None:
|
|
||||||
if hasattr(self, key):
|
|
||||||
if key in self.int_dict:
|
|
||||||
value = int(value)
|
|
||||||
elif key in self.bool_dict:
|
|
||||||
value = bool(value)
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
async def updates(self, options) -> None:
|
|
||||||
with open('data/.env', 'w', encoding='utf-8') as f:
|
|
||||||
for i, key, value in options:
|
|
||||||
# 更新env文件
|
|
||||||
f.write(f"{key}={value}\n")
|
|
||||||
# 更新配置
|
|
||||||
await self.update(key, value)
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
|
||||||
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 498 KiB |
|
Before Width: | Height: | Size: 127 KiB |
@@ -1,335 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<link rel="stylesheet" href="/static/asserts/index.css">
|
|
||||||
<link rel="shortcut icon" href="/static/asserts/favicon.ico" type="image/x-icon"/>
|
|
||||||
<title>后台管理-{{title}}</title>
|
|
||||||
<meta name="description" content="{{description}}"/>
|
|
||||||
<meta name="keywords" content="{{keywords}}"/>
|
|
||||||
<meta name="generator" content="FileCodeBox"/>
|
|
||||||
<meta name="template" content="Lan"/>
|
|
||||||
<script src="/static/asserts/vue.min.js"></script>
|
|
||||||
<script src="/static/asserts/index.js"></script>
|
|
||||||
<style>
|
|
||||||
.el-menu-demo {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<el-row v-if="login" :gutter="10">
|
|
||||||
<el-col :span="24" style="text-align: center">
|
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect">
|
|
||||||
<el-menu-item index="2">分享文件管理</el-menu-item>
|
|
||||||
<el-menu-item index="4">系统配置</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</el-col>
|
|
||||||
<el-col v-if="activeIndex === '2'">
|
|
||||||
<el-card style="height: 80vh;overflow: scroll;margin-bottom: 1rem">
|
|
||||||
<el-empty v-if="files.length === 0" description="暂时还没有文件"></el-empty>
|
|
||||||
<el-card v-for="file in files" :key="file.code">
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="20">
|
|
||||||
<div style="cursor: pointer;text-align: left">
|
|
||||||
<div>取件码:${ file.code }</div>
|
|
||||||
<div>文件名:${ file.name }</div>
|
|
||||||
<div>次 数:${ file.count }</div>
|
|
||||||
<div v-if="file.exp_time">到 期:${ file.exp_time.slice(0,19) }</div>
|
|
||||||
<div v-else>到 期:永不过期</div>
|
|
||||||
<div v-if="file.name==='文本分享'">
|
|
||||||
<span style="white-space: pre-line">内 容:${ file.text }</span>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<span>链 接:</span>
|
|
||||||
<a :href="file.text" target="_blank"
|
|
||||||
style="color: #1E9FFF;text-underline: none"
|
|
||||||
type="primary">点击下载</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="4">
|
|
||||||
<el-button type="danger" @click="deleteFile(file.code)">删除</el-button>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-card>
|
|
||||||
</el-card>
|
|
||||||
<el-pagination
|
|
||||||
background
|
|
||||||
style="text-align: center"
|
|
||||||
layout="prev, pager, next, sizes"
|
|
||||||
:page-size="paginate.page_size"
|
|
||||||
@current-change="updatePage"
|
|
||||||
@size-change="updateSize"
|
|
||||||
:total="paginate.total"
|
|
||||||
>
|
|
||||||
</el-pagination>
|
|
||||||
</el-col>
|
|
||||||
<el-col v-if="activeIndex === '4'">
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="4" style="height: 88vh;overflow: scroll;text-align: center" :gutter="10">
|
|
||||||
<el-card v-for="menu in menus" :key="menu.key">
|
|
||||||
<div style="cursor: pointer;" @click="updateTab=menu.key">${menu.name}</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="20">
|
|
||||||
<el-card style="height: 88vh;overflow: scroll">
|
|
||||||
<div v-if="updateTab=='INSTALL'">
|
|
||||||
<div>
|
|
||||||
<h1>FileCodeBox V${config.INSTALL}</h1>
|
|
||||||
<h2>Github:<a target="_blank"
|
|
||||||
href="https://github.com/vastsa/FileCodeBox">FileCodeBox</a></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="updateTab=='BANNERS'">
|
|
||||||
<el-form ref="form" style="margin:1rem auto" :model="config" label-width="200px">
|
|
||||||
<el-form-item v-for="(banner,index) in config.BANNERS" :label="'Banner'+ (index+1)">
|
|
||||||
<div style="display: inline-block">
|
|
||||||
<el-input v-model="banner.text" placeholder="文本"></el-input>
|
|
||||||
<el-input v-model="banner.url" placeholder="跳转链接"></el-input>
|
|
||||||
<el-input v-model="banner.src" placeholder="图片路径"></el-input>
|
|
||||||
</div>
|
|
||||||
<div style="display: inline-block">
|
|
||||||
<el-button type="danger" @click="deleteBanner(index)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button size="mini" @click="addBanner" type="primary">新增Banner</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="updateConfig" type="primary">更新</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="updateTab=='WEBSITE'">
|
|
||||||
<el-form ref="form" style="margin:1rem auto" :model="config" label-width="200px">
|
|
||||||
<el-form-item label="后台密码">
|
|
||||||
<el-input type="password" v-model="config.ADMIN_PASSWORD"
|
|
||||||
placeholder="网站名称"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="网站名称">
|
|
||||||
<el-input v-model="config.TITLE" placeholder="网站名称"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="网站描述">
|
|
||||||
<el-input v-model="config.DESCRIPTION" placeholder="网站描述"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="网站关键词">
|
|
||||||
<el-input v-model="config.KEYWORDS" placeholder="网站关键词"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-alert
|
|
||||||
title="下列信息修改后需要手动重启系统"
|
|
||||||
type="warning">
|
|
||||||
</el-alert>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="系统端口">
|
|
||||||
<el-input v-model="config.PORT" placeholder="系统端口"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="后台地址">
|
|
||||||
<el-input v-model="config.ADMIN_ADDRESS" placeholder="后台地址"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="存储引擎">
|
|
||||||
<el-input v-model="config.STORAGE_ENGINE" placeholder="存储引擎"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="updateConfig" type="primary">更新</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="updateTab=='SHARE'">
|
|
||||||
<el-form ref="form" style="margin:1rem auto" :model="config" label-width="200px">
|
|
||||||
<el-form-item label="开启上传">
|
|
||||||
<el-input v-model="config.ENABLE_UPLOAD" placeholder="开启上传"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最长天数">
|
|
||||||
<el-input v-model="config.MAX_DAYS" placeholder="最长允许保留多少天"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="删除过期间隔">
|
|
||||||
<el-input v-model="config.DELETE_EXPIRE_FILES_INTERVAL"
|
|
||||||
placeholder="删除过期文件的间隔(分钟)"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="上传大小限制">
|
|
||||||
<small>单位:(bit),1mb=1 * 1024 * 1024</small>
|
|
||||||
<el-input v-model="config.FILE_SIZE_LIMIT"
|
|
||||||
placeholder="上传大小限制(bit)"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-alert
|
|
||||||
title="下列信息修改后需要手动重启系统"
|
|
||||||
type="warning">
|
|
||||||
</el-alert>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="口令错误限制">
|
|
||||||
<small>每几分钟</small>
|
|
||||||
<el-input v-model="config.ERROR_MINUTE"
|
|
||||||
placeholder="错误次数过多后限制时间"></el-input>
|
|
||||||
<small>允许错误几次</small>
|
|
||||||
<el-input v-model="config.ERROR_COUNT" placeholder="允许错误次数"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="上传次数限制">
|
|
||||||
<small>每几分钟</small>
|
|
||||||
<el-input v-model="config.UPLOAD_MINUTE"
|
|
||||||
placeholder="每几分钟"></el-input>
|
|
||||||
<small>允许上传几次</small>
|
|
||||||
<el-input v-model="config.UPLOAD_COUNT"
|
|
||||||
placeholder="允许次数"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="updateConfig" type="primary">更新</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<div v-else style="width: 250px;text-align: center;margin: 40vh auto">
|
|
||||||
<el-input v-model="pwd" placeholder="请输入登录密码">
|
|
||||||
<el-button slot="append" type="primary" @click="loginAdmin">登录</el-button>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
<script src="/static/asserts/axios.min.js"></script>
|
|
||||||
<script>
|
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
|
||||||
delimiters: ['${', '}'],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
login: false,
|
|
||||||
pwd: '',
|
|
||||||
activeIndex: '2',
|
|
||||||
config: {
|
|
||||||
banners: []
|
|
||||||
},
|
|
||||||
menus: [],
|
|
||||||
updateTab: 'INSTALL',
|
|
||||||
files: [],
|
|
||||||
locals: [],
|
|
||||||
paginate: {
|
|
||||||
page: 1,
|
|
||||||
size: 10,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
const pwd = localStorage.getItem('pwd');
|
|
||||||
if (pwd) {
|
|
||||||
login = true;
|
|
||||||
this.pwd = pwd;
|
|
||||||
this.loginAdmin();
|
|
||||||
this.getLocalFiles();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getConfig: function () {
|
|
||||||
axios.get(window.location.href + '/config', {
|
|
||||||
headers: {
|
|
||||||
pwd: this.pwd
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.config = res.data.data;
|
|
||||||
this.menus = res.data.menus;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateSize: function (value) {
|
|
||||||
this.paginate.size = value;
|
|
||||||
this.loginAdmin();
|
|
||||||
},
|
|
||||||
updatePage: function (value) {
|
|
||||||
this.paginate.page = value;
|
|
||||||
this.loginAdmin();
|
|
||||||
},
|
|
||||||
deleteBanner: function (index) {
|
|
||||||
this.config.BANNERS.splice(index, 1);
|
|
||||||
},
|
|
||||||
addBanner: function () {
|
|
||||||
this.config.BANNERS.push({
|
|
||||||
'url': '',
|
|
||||||
'src': '',
|
|
||||||
'text': '',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateConfig: function () {
|
|
||||||
axios.patch('', this.config, {
|
|
||||||
'headers': {
|
|
||||||
'pwd': this.pwd,
|
|
||||||
'Content-Type': 'multipart/json'
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.$message({
|
|
||||||
message: res.data.detail,
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loginAdmin: function () {
|
|
||||||
axios.post('', this.paginate, {
|
|
||||||
'headers': {
|
|
||||||
'pwd': this.pwd,
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.paginate = res.data.paginate;
|
|
||||||
this.files = res.data.data
|
|
||||||
this.login = true;
|
|
||||||
localStorage.setItem('pwd', this.pwd);
|
|
||||||
this.getConfig();
|
|
||||||
}).catch(e => {
|
|
||||||
localStorage.clear()
|
|
||||||
this.$message({'message': e.response.data.detail, 'type': 'error'});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleSelect(key, keyPath) {
|
|
||||||
this.activeIndex = key;
|
|
||||||
},
|
|
||||||
deleteFile: function (code) {
|
|
||||||
axios.delete('?code=' + code, {'headers': {'pwd': this.pwd}}).then(res => {
|
|
||||||
this.files = this.files.filter(item => item.code !== code)
|
|
||||||
this.$message({
|
|
||||||
message: res.data.detail,
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getLocalFiles: function () {
|
|
||||||
axios.get(window.location.href + '/files', {
|
|
||||||
'headers': {
|
|
||||||
'pwd': this.pwd,
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.locals = res.data.data;
|
|
||||||
})
|
|
||||||
},
|
|
||||||
shareFile: function (file) {
|
|
||||||
axios.post(window.location.href + '/files', file, {
|
|
||||||
'headers': {
|
|
||||||
'pwd': this.pwd,
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.getLocalFiles()
|
|
||||||
this.$alert(`请在24小时内使用:${res.data.data.code}`, '分享成功', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</html>
|
|
||||||
@@ -1,576 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<link rel="stylesheet" href="/static/asserts/index.css">
|
|
||||||
<link rel="stylesheet" href="/static/asserts/github-markdown-css/github-markdown.css">
|
|
||||||
<link rel="shortcut icon" href="/static/asserts/favicon.ico" type="image/x-icon"/>
|
|
||||||
<title>{{title}}</title>
|
|
||||||
<meta name="description" content="{{description}}"/>
|
|
||||||
<meta name="keywords" content="{{keywords}}"/>
|
|
||||||
<meta name="generator" content="FileCodeBox"/>
|
|
||||||
<meta name="template" content="V1.7 Beta"/>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qu .el-button {
|
|
||||||
width: 6rem;
|
|
||||||
height: 6rem;
|
|
||||||
margin: 0.2rem;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon-circle-close::before {
|
|
||||||
font-size: 26px !important;
|
|
||||||
line-height: 128px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qu .el-input__inner {
|
|
||||||
height: 100px;
|
|
||||||
margin: 1rem 0;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card {
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background-color: #18181c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card, .el-textarea__inner, .el-upload-dragger {
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
box-shadow: 5px 5px 0 0 rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#app * {
|
|
||||||
color: #ccc !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input-group__prepend, .el-input__inner, .el-input-group__append, .el-empty__description, .el-select-dropdown, .el-button {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button:focus, .el-button:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button:active, .el-select-dropdown__item.hover, .el-select-dropdown__item:hover, .el-input.is-disabled .el-input__inner {
|
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-drawer, #el-drawer__title, .el-drawer__body, .el-drawer__wrapper .el-loading-mask {
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-loading-mask {
|
|
||||||
background-color: rgba(0, 0, 0, 0.8) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<script src="/static/asserts/vue.min.js"></script>
|
|
||||||
<script src="/static/asserts/index.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app" style="text-align: center">
|
|
||||||
<el-row v-if="pageNum === 0" class="qu" style="max-width:400px;margin: 4rem auto 0 auto">
|
|
||||||
<el-card class="glass" style="padding-bottom: 1rem">
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-input round autofocus @change="changeInput" clearable v-model:value="code" maxlength="5"
|
|
||||||
:disabled="inputDisable" placeholder="请输入五位取件码">
|
|
||||||
</el-input>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('1')">1</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('2')">2</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('3')">3</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('4')">4</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('5')">5</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('6')">6</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('7')">7</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('8')">8</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('9')">9</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="pageNum=1">
|
|
||||||
<div style="height: 10px" class="el-icon-upload2"></div>
|
|
||||||
</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="listenInput('0')">0</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span=8>
|
|
||||||
<el-button round @click="quDrawer=true">
|
|
||||||
<div class="el-icon-takeaway-box"></div>
|
|
||||||
</el-button>
|
|
||||||
</el-col>
|
|
||||||
</el-card>
|
|
||||||
</el-row>
|
|
||||||
<el-row v-else style="max-width:400px;margin: 4rem auto 0 auto">
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-card :body-style="{ padding: '0px 0px 20px 0px' }">
|
|
||||||
<div class="block" style="margin-bottom: 1rem">
|
|
||||||
<el-carousel height="150px">
|
|
||||||
<el-carousel-item v-for="item in banners" :key="item">
|
|
||||||
<a :href="item.url" target="_blank">
|
|
||||||
<img class="image" style="width: 400px" :src="item.src" :alt="item.text">
|
|
||||||
</a>
|
|
||||||
</el-carousel-item>
|
|
||||||
</el-carousel>
|
|
||||||
</div>
|
|
||||||
<el-row>
|
|
||||||
<el-input style="width: 190px" :disabled="uploadData.exp_style==='forever'" placeholder="数量"
|
|
||||||
v-model="uploadData.exp_value"
|
|
||||||
class="input-with-select">
|
|
||||||
<el-select style="width: 75px" v-model="uploadData.exp_style" slot="prepend"
|
|
||||||
placeholder="请选择">
|
|
||||||
<el-option label="天数" value="day"></el-option>
|
|
||||||
<el-option label="小时" value="hour"></el-option>
|
|
||||||
<el-option label="分钟" value="minute"></el-option>
|
|
||||||
<el-option label="永久" value="forever"></el-option>
|
|
||||||
<el-option label="次数" value="count"></el-option>
|
|
||||||
</el-select>
|
|
||||||
<el-button slot="append" disabled>${exp_style_dict[uploadData.exp_style]}</el-button>
|
|
||||||
</el-input>
|
|
||||||
<el-radio-group style="margin-left: 18px" v-model="uploadData.is_file">
|
|
||||||
<el-radio label="1">
|
|
||||||
文件
|
|
||||||
</el-radio>
|
|
||||||
<el-radio label="0">
|
|
||||||
文本
|
|
||||||
</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-row>
|
|
||||||
<el-upload
|
|
||||||
v-if="uploadData.is_file === '1'"
|
|
||||||
drag
|
|
||||||
v-loading="uploading"
|
|
||||||
action="/share"
|
|
||||||
:element-loading-text="loadingText"
|
|
||||||
element-loading-spinner="el-icon-loading"
|
|
||||||
multiple
|
|
||||||
:disabled="uploading"
|
|
||||||
:http-request="uploadFile"
|
|
||||||
style="margin: 1rem 0;"
|
|
||||||
:data="uploadData"
|
|
||||||
:before-upload="beforeUpload"
|
|
||||||
:headers="{'pwd':pwd}"
|
|
||||||
:on-success="successUpload"
|
|
||||||
:on-error="errorUpload"
|
|
||||||
>
|
|
||||||
<i class="el-icon-upload"></i>
|
|
||||||
<div class="el-upload__text">将文字、文件拖、粘贴到此处,或<em>点击上传</em></div>
|
|
||||||
<div class="el-upload__text" style="font-size: 10px">天数<7或限制次数(24h后删除)</div>
|
|
||||||
</el-upload>
|
|
||||||
<el-input
|
|
||||||
v-else
|
|
||||||
type="textarea"
|
|
||||||
:autosize="{ minRows: 8}"
|
|
||||||
style="margin: 1rem 0"
|
|
||||||
placeholder="请输入内容,使用按钮存入"
|
|
||||||
v-model="uploadData.text">
|
|
||||||
</el-input>
|
|
||||||
<div class="el-upload__tip">
|
|
||||||
<el-button round @click="pageNum=0">
|
|
||||||
<div class="el-icon-back"></div>
|
|
||||||
取件
|
|
||||||
</el-button>
|
|
||||||
<el-button round @click="jiDrawer=true">
|
|
||||||
<div class="el-icon-takeaway-box"></div>
|
|
||||||
我的文件
|
|
||||||
</el-button>
|
|
||||||
<el-button round v-if="uploadData.is_file === '0'" @click="uploadText">
|
|
||||||
<div class="el-icon-upload2"></div>
|
|
||||||
存入
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-drawer
|
|
||||||
title="文件箱"
|
|
||||||
:visible.sync="quDrawer"
|
|
||||||
:direction="direction"
|
|
||||||
size="50%"
|
|
||||||
>
|
|
||||||
<el-card v-for="(file,index) in quFiles" :key="index" class="box-card">
|
|
||||||
<div style="cursor: pointer;text-align: left;margin-left: 2rem">
|
|
||||||
<div>取件码:${ file.code }</div>
|
|
||||||
<div>文件名:${ file.name }</div>
|
|
||||||
<div v-if="file.name==='文本分享'">
|
|
||||||
<article v-html="file.text" class="markdown-body" @click="copyText(file.text,1)"></article>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<span>链 接:</span>
|
|
||||||
<a :href="file.text" target="_blank"
|
|
||||||
style="color: #1E9FFF;text-underline: none"
|
|
||||||
type="primary">点击下载</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
<el-empty v-if="quFiles.length===0" description="请输入取件码,收取文件"></el-empty>
|
|
||||||
</el-drawer>
|
|
||||||
<el-drawer
|
|
||||||
title="文件箱"
|
|
||||||
:visible.sync="jiDrawer"
|
|
||||||
:direction="direction"
|
|
||||||
size="50%">
|
|
||||||
<el-empty v-if="jiFiles.length===0" description="请上传文件"></el-empty>
|
|
||||||
<el-card style="margin-top: 0.2rem" v-for="(file,index) in jiFiles" :key="index">
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="15">
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="24" style="line-height: 45px">
|
|
||||||
${ file.name }
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
取件码:
|
|
||||||
<h1 @click="copyText(file.code,0)" style="margin: 0;display: inline;cursor: pointer">
|
|
||||||
${ file.code }
|
|
||||||
</h1>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="4">
|
|
||||||
<img style="width: 80px;height: 80px;"
|
|
||||||
:src="qrcodeUrl(file)" alt="二维码" :alt="file.code">
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-card>
|
|
||||||
</el-drawer>
|
|
||||||
<div style="text-align: center; margin-top: 1rem;color: #606266">
|
|
||||||
<a style="text-decoration: none;color: #606266" target="_blank" href="https://github.com/vastsa/FileCodeBox">FileCodeBox
|
|
||||||
V1.7 Beta</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
<script src="/static/asserts/axios.min.js"></script>
|
|
||||||
<script>
|
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
|
||||||
delimiters: ['${', '}'],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
code: '',
|
|
||||||
quDrawer: false,
|
|
||||||
jiDrawer: false,
|
|
||||||
direction: 'btt',
|
|
||||||
exp_style_dict: {
|
|
||||||
'day': '天',
|
|
||||||
'hour': '时',
|
|
||||||
'minute': '分',
|
|
||||||
'forever': '无',
|
|
||||||
'count': '次',
|
|
||||||
},
|
|
||||||
quFiles: [],
|
|
||||||
jiFiles: [],
|
|
||||||
uploadGroup: [],
|
|
||||||
pageNum: 0,
|
|
||||||
inputDisable: false,
|
|
||||||
fileSizeLimit: '{{fileSizeLimit}}',
|
|
||||||
pwd: localStorage.getItem('pwd') || '',
|
|
||||||
banners: [],
|
|
||||||
enableUpload: false,
|
|
||||||
uploadData: {
|
|
||||||
exp_style: 'day',
|
|
||||||
exp_value: 1,
|
|
||||||
type: 'text',
|
|
||||||
name: '文本分享',
|
|
||||||
is_file: '1',
|
|
||||||
text: '',
|
|
||||||
size: 0,
|
|
||||||
},
|
|
||||||
uploading: false,
|
|
||||||
loadingText: '正在上传中...',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
// 进入网站时,判断Get是否有code参数,有则直接进行取件操作
|
|
||||||
let code = window.location.search.substring('code=='.length)
|
|
||||||
if (code) {
|
|
||||||
this.code = code
|
|
||||||
this.getFile()
|
|
||||||
}
|
|
||||||
// 剪切板监听
|
|
||||||
const that = this
|
|
||||||
document.addEventListener('paste', function (event) {
|
|
||||||
if (that.pageNum === 1) {
|
|
||||||
const items = event.clipboardData && event.clipboardData.items;
|
|
||||||
if (items && items.length) {
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].kind === 'string') {
|
|
||||||
if (items[i].type.match(/^text\/plain/) && that.uploadData.type !== '2') {
|
|
||||||
that.$message('剪切板文字正在上传,请稍等');
|
|
||||||
items[i].getAsString(function (str) {
|
|
||||||
that.uploadData.text = str;
|
|
||||||
that.uploadText();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const file = items[i].getAsFile();
|
|
||||||
that.$message('剪切板文件正在上传,请稍等');
|
|
||||||
that.uploadFile({file: file});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 获取Banner
|
|
||||||
axios.get('/banner', {
|
|
||||||
headers: {
|
|
||||||
pwd: localStorage.getItem('pwd')
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.enableUpload = res.data.enable
|
|
||||||
this.banners = res.data.data
|
|
||||||
})
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
code: function (code) {
|
|
||||||
if (code.length === 5) {
|
|
||||||
this.inout_disable = true;
|
|
||||||
this.getFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
http: function (method, url, data = {}, config = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios({
|
|
||||||
method: method,
|
|
||||||
url: url,
|
|
||||||
data: data,
|
|
||||||
...config
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.detail) {
|
|
||||||
this.$message({
|
|
||||||
message: res.data.detail,
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
resolve(res.data)
|
|
||||||
}).catch(err => {
|
|
||||||
if (err.response.data.detail) {
|
|
||||||
this.$message({
|
|
||||||
message: err.response.data.detail,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
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).then((res) => {
|
|
||||||
this.upload_groups[chunk_index - 1] = 1;
|
|
||||||
}).catch((res) => {
|
|
||||||
this.upload_groups[chunk_index - 1] = 2;
|
|
||||||
})
|
|
||||||
},
|
|
||||||
copyText: function (value, style = 1) {
|
|
||||||
if (style === 0) {
|
|
||||||
value = `我给你分享了文件,取件码:${value},${window.location.href}?code=${value}`;
|
|
||||||
}
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.setAttribute('readonly', 'readonly');
|
|
||||||
input.setAttribute('value', value);
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.select();
|
|
||||||
if (document.execCommand('copy')) {
|
|
||||||
document.execCommand('copy');
|
|
||||||
this.$message({
|
|
||||||
message: '复制成功,快去分享给你的朋友吧',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.body.removeChild(input);
|
|
||||||
},
|
|
||||||
getFile: function () {
|
|
||||||
const that = this;
|
|
||||||
this.http('post', `?code=${that.code}`).then(res => {
|
|
||||||
that.quDrawer = true;
|
|
||||||
that.quFiles.unshift(res.data);
|
|
||||||
})
|
|
||||||
that.code = '';
|
|
||||||
that.input_disable = false
|
|
||||||
},
|
|
||||||
qrcodeUrl(file) {
|
|
||||||
return 'https://api.qrserver.com/v1/create-qr-code/?data=' + window.location.href + '?code=' + file.code
|
|
||||||
},
|
|
||||||
listenInput: function (value) {
|
|
||||||
if (this.code.length < 5) {
|
|
||||||
this.code += value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
changeInput: function (value) {
|
|
||||||
this.code = value;
|
|
||||||
},
|
|
||||||
checkFile: function (file) {
|
|
||||||
if (!this.enableUpload) {
|
|
||||||
this.$message({
|
|
||||||
message: '上传功能已关闭',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (file != null) {
|
|
||||||
if (file.size > this.fileSizeLimit) {
|
|
||||||
this.$message({
|
|
||||||
message: `文件大小不能超过${this.fileSizeLimit/1024/1024}MB`,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
uploadText: function () {
|
|
||||||
if (this.uploadData.text === '') {
|
|
||||||
this.$message({
|
|
||||||
message: '请先粘贴文字',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.http('post', '/share/text/', this.uploadData, {
|
|
||||||
headers: {
|
|
||||||
'pwd': this.pwd
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.jiFiles.unshift(res.data);
|
|
||||||
this.jiDrawer = true;
|
|
||||||
this.uploadData.text = '';
|
|
||||||
this.$message({
|
|
||||||
message: '上传成功',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uploadFile: async function (e) {
|
|
||||||
const that = this;
|
|
||||||
if (this.checkFile(e.file)) {
|
|
||||||
that.uploading = true;
|
|
||||||
that.loadingText = '正在上传中...';
|
|
||||||
this.http('post', '/file/create/').then(async res => {
|
|
||||||
const file = e.file;
|
|
||||||
let chunk_index = 0;
|
|
||||||
const shardSize = 1024 * 1024 * 5;
|
|
||||||
const {name, size, type} = file;
|
|
||||||
const total_chunks = Math.ceil(size / shardSize);
|
|
||||||
this.upload_groups = [];
|
|
||||||
for (let i = 0; i < total_chunks; i++) {
|
|
||||||
this.upload_groups.push(0);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
let success = 0;
|
|
||||||
let flag = true;
|
|
||||||
for (let i = 0; i < total_chunks; i++) {
|
|
||||||
if (this.upload_groups[i] === 0) {
|
|
||||||
flag = false;
|
|
||||||
} else if (this.upload_groups[i] === 2) {
|
|
||||||
flag = false;
|
|
||||||
start = chunk_index * shardSize;
|
|
||||||
const end = Math.min(start + shardSize, size);
|
|
||||||
this.uploadChunk(file.slice(start, end), res.data, chunk_index, total_chunks);
|
|
||||||
} else {
|
|
||||||
success += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
that.loadingText = `已上传${ (success/total_chunks*100).toFixed(2) }%`;
|
|
||||||
if (flag) {
|
|
||||||
clearInterval(interval);
|
|
||||||
this.http('get', `/file/merge/${res.data}/?file_name=${name}&total_chunks=${total_chunks}`).then(text => {
|
|
||||||
this.http('post', '/share/file/', {
|
|
||||||
text: text.data,
|
|
||||||
size: size,
|
|
||||||
exp_style: this.uploadData.exp_style,
|
|
||||||
exp_value: this.uploadData.exp_value,
|
|
||||||
type: type,
|
|
||||||
name: name,
|
|
||||||
key: res.data,
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
'pwd': this.pwd
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
this.jiFiles.unshift(res.data);
|
|
||||||
this.jiDrawer = true;
|
|
||||||
this.uploadData.text = '';
|
|
||||||
this.uploadData.file = null;
|
|
||||||
that.uploading = false;
|
|
||||||
this.$message({
|
|
||||||
message: '上传成功',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
successUpload(response, file, fileList) {
|
|
||||||
if (response) {
|
|
||||||
this.$message({
|
|
||||||
message: response.detail,
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
this.jiFiles.unshift(response.data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
errorUpload(error, file, fileList) {
|
|
||||||
error = JSON.parse(error.message)
|
|
||||||
this.$message({
|
|
||||||
message: error.detail,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
beforeUpload: function (file) {
|
|
||||||
return this.checkFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</html>
|
|
||||||