update
@@ -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
|
||||
@@ -1,52 +0,0 @@
|
||||
from typing import Union
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import Header, HTTPException, Request
|
||||
|
||||
from settings import settings
|
||||
|
||||
|
||||
async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None):
|
||||
if 'share' in request.url.path:
|
||||
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:
|
||||
def __init__(self, count, minutes):
|
||||
self.ips = {}
|
||||
self.count = count
|
||||
self.minutes = minutes
|
||||
|
||||
def check_ip(self, ip):
|
||||
# 检查ip是否被禁止
|
||||
if ip in self.ips:
|
||||
if self.ips[ip]['count'] >= self.count:
|
||||
if self.ips[ip]['time'] + timedelta(minutes=self.minutes) > datetime.now():
|
||||
return False
|
||||
else:
|
||||
self.ips.pop(ip)
|
||||
return True
|
||||
|
||||
def add_ip(self, ip):
|
||||
ip_info = self.ips.get(ip, {'count': 0, 'time': datetime.now()})
|
||||
ip_info['count'] += 1
|
||||
ip_info['time'] = datetime.now()
|
||||
self.ips[ip] = ip_info
|
||||
return ip_info['count']
|
||||
|
||||
async def remove_expired_ip(self):
|
||||
for ip in list(self.ips.keys()):
|
||||
if self.ips[ip]['time'] + timedelta(minutes=self.minutes) < datetime.now():
|
||||
self.ips.pop(ip)
|
||||
|
||||
def __call__(self, request: Request):
|
||||
ip = request.headers.get('X-Real-IP', request.headers.get('X-Forwarded-For', request.client.host))
|
||||
if not self.check_ip(ip):
|
||||
raise HTTPException(status_code=400, detail=f"请求次数过多,请稍后再试")
|
||||
return ip
|
||||
@@ -1,174 +0,0 @@
|
||||
import os
|
||||
import asyncio
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO
|
||||
from fastapi import UploadFile
|
||||
|
||||
from core.database import Codes
|
||||
from settings import settings
|
||||
|
||||
if settings.STORAGE_ENGINE == 'aliyunsystem':
|
||||
try:
|
||||
import oss2
|
||||
except ImportError:
|
||||
os.system('pip install oss2')
|
||||
import oss2
|
||||
|
||||
|
||||
class FileSystemStorage:
|
||||
def __init__(self):
|
||||
self.DATA_ROOT = Path(settings.DATA_ROOT)
|
||||
self.STATIC_URL = settings.STATIC_URL
|
||||
self.NAME = "filesystem"
|
||||
self.DOWN_PATH = '/select'
|
||||
|
||||
async def get_filepath(self, text: str):
|
||||
return self.DATA_ROOT / text.lstrip(self.STATIC_URL + '/')
|
||||
|
||||
async def get_url(self, info: Codes):
|
||||
return f'{self.DOWN_PATH}?code={info.code}'
|
||||
|
||||
async def get_text(self, file: UploadFile, key: str):
|
||||
ext = file.filename.split('.')[-1]
|
||||
now = datetime.now()
|
||||
path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/"
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
text = f"{self.STATIC_URL}/{(path / f'{key}.{ext}').relative_to(self.DATA_ROOT)}"
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
async def get_size(file: UploadFile):
|
||||
f = file.file
|
||||
f.seek(0, os.SEEK_END)
|
||||
size = f.tell()
|
||||
f.seek(0, os.SEEK_SET)
|
||||
return size
|
||||
|
||||
@staticmethod
|
||||
def _save(filepath, file: BinaryIO):
|
||||
with open(filepath, 'wb') as f:
|
||||
chunk_size = 256 * 1024
|
||||
chunk = file.read(chunk_size)
|
||||
while chunk:
|
||||
f.write(chunk)
|
||||
chunk = file.read(chunk_size)
|
||||
|
||||
@staticmethod
|
||||
def _save_chunk(filepath, file: bytes):
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(file)
|
||||
|
||||
async def create_upload_file(self):
|
||||
file_key = uuid.uuid4().hex
|
||||
file_path = self.DATA_ROOT / f"temp/{file_key}"
|
||||
if not file_path.exists():
|
||||
file_path.mkdir(parents=True)
|
||||
return file_key
|
||||
|
||||
async def save_chunk_file(self, file_key, file_chunk, chunk_index, chunk_total):
|
||||
file_path = self.DATA_ROOT / f"temp/{file_key}/"
|
||||
await asyncio.to_thread(self._save_chunk, file_path / f"{chunk_total}-{chunk_index}.temp", file_chunk)
|
||||
|
||||
async def merge_chunks(self, file_key, file_name, total_chunks: int):
|
||||
ext = file_name.split('.')[-1]
|
||||
now = datetime.now()
|
||||
path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/"
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
text = f"{self.STATIC_URL}/{(path / f'{file_key}.{ext}').relative_to(self.DATA_ROOT)}"
|
||||
with open(path / f'{file_key}.{ext}', 'wb') as f:
|
||||
for i in range(1, total_chunks + 1):
|
||||
now_temp = self.DATA_ROOT / f'temp/{file_key}/{total_chunks}-{i}.temp'
|
||||
with open(now_temp, 'rb') as r:
|
||||
f.write(r.read())
|
||||
await asyncio.to_thread(os.remove, now_temp)
|
||||
await asyncio.to_thread(os.rmdir, self.DATA_ROOT / f'temp/{file_key}/')
|
||||
return text
|
||||
|
||||
async def save_file(self, file: UploadFile, text: str):
|
||||
filepath = await self.get_filepath(text)
|
||||
await asyncio.to_thread(self._save, filepath, file.file)
|
||||
|
||||
async def delete_file(self, text: str):
|
||||
filepath = await self.get_filepath(text)
|
||||
if filepath.exists():
|
||||
await asyncio.to_thread(os.remove, filepath)
|
||||
await asyncio.to_thread(self.judge_delete_folder, filepath)
|
||||
|
||||
async def delete_files(self, texts):
|
||||
tasks = [self.delete_file(text) for text in texts]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
def judge_delete_folder(self, filepath):
|
||||
current = filepath.parent
|
||||
while current != self.DATA_ROOT:
|
||||
if not list(current.iterdir()):
|
||||
os.rmdir(current)
|
||||
current = current.parent
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
class AliyunFileStorage:
|
||||
def __init__(self):
|
||||
auth = oss2.Auth(settings.KeyId, settings.KeySecret)
|
||||
self.bucket = oss2.Bucket(auth, settings.OSS_ENDPOINT, settings.BUCKET_NAME)
|
||||
|
||||
def upload_file(self, upload_filepath, remote_filepath):
|
||||
self.bucket.put_object_from_file(remote_filepath, upload_filepath)
|
||||
|
||||
async def get_text(self, file: UploadFile, key: str):
|
||||
ext = file.filename.split('.')[-1]
|
||||
now = datetime.now()
|
||||
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):
|
||||
text = info.text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/")
|
||||
url = self.bucket.sign_url('GET', text, settings.ACCESSTIME, slash_safe=True)
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
async def get_size(file: UploadFile):
|
||||
f = file.file
|
||||
f.seek(0, os.SEEK_END)
|
||||
size = f.tell()
|
||||
f.seek(0, os.SEEK_SET)
|
||||
return size
|
||||
|
||||
@staticmethod
|
||||
def _save(filepath, file: BinaryIO):
|
||||
with open(filepath, 'wb') as f:
|
||||
chunk_size = 256 * 1024
|
||||
chunk = file.read(chunk_size)
|
||||
while chunk:
|
||||
f.write(chunk)
|
||||
chunk = file.read(chunk_size)
|
||||
|
||||
async def save_file(self, file: UploadFile, 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 = {
|
||||
"filesystem": FileSystemStorage,
|
||||
"aliyunsystem": AliyunFileStorage
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import random
|
||||
import asyncio
|
||||
import string
|
||||
import time
|
||||
|
||||
from sqlalchemy import or_, select, delete
|
||||
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():
|
||||
return random.randint(10000, 99999)
|
||||
|
||||
|
||||
async def get_random_string():
|
||||
r_s = string.ascii_letters + string.digits
|
||||
return ''.join(random.choice(r_s) for _ in range(5)).upper()
|
||||
|
||||
|
||||
async def get_code(s: AsyncSession, exp_style):
|
||||
if exp_style == 'forever':
|
||||
generate = get_random_string
|
||||
else:
|
||||
generate = get_random_num
|
||||
code = await generate()
|
||||
while (await s.execute(select(Codes.id).where(Codes.code == code))).scalar():
|
||||
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
|
||||
|
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 +0,0 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select, func, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse, FileResponse, RedirectResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from core.utils import error_ip_limit, upload_ip_limit, storage, delete_expire_files, get_token, \
|
||||
get_expire_info
|
||||
from core.depends import admin_required
|
||||
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(debug=settings.DEBUG, redoc_url=None, )
|
||||
|
||||
|
||||
@app.on_event('startup')
|
||||
async def startup(s: AsyncSession = Depends(get_session)):
|
||||
# 初始化数据库
|
||||
await init_models(s)
|
||||
# 启动后台任务,不定时删除过期文件
|
||||
asyncio.create_task(delete_expire_files())
|
||||
|
||||
|
||||
# 数据存储文件夹
|
||||
DATA_ROOT = Path(settings.DATA_ROOT)
|
||||
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('/')
|
||||
async def index():
|
||||
return HTMLResponse(
|
||||
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>
|
||||