This commit is contained in:
lan
2023-08-11 19:23:48 +08:00
parent 100fcb4551
commit 25f94fc0bc
39 changed files with 0 additions and 5316 deletions
View File
View File
-84
View File
@@ -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
-52
View File
@@ -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
-174
View File
@@ -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
}
-83
View File
@@ -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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

-247
View File
@@ -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)
-308
View File
@@ -1,308 +0,0 @@
<div align="center">
<h1>文件快递柜-轻量</h1>
<h2>FileCodeBox-Lite</h2>
<p><em>匿名口令分享文本,文件,像拿快递一样取文件</em></p>
<p>交流Q群:739673698,欢迎各位集思广益,项目构思重构中</p>
</div>
![banner](https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/static/banners/img_1.png)
---
[简体中文](./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": "名称"
}
}
```
## 状态
![Alt](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg "Repobeats analytics image")
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](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
## 免责声明
本项目开源仅供学习使用,不得用于任何违法用途,否则后果自负,与本人无关。使用请保留项目地址谢谢。
-230
View File
@@ -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>
![banner](https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/static/banners/img_1.png)
---
[简体中文](./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
![Alt](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg "Repobeats analytics image")
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](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.
-9
View File
@@ -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
-88
View File
@@ -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()
-2
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

-11
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

-335
View File
@@ -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>&nbsp;&nbsp; ${ file.count }</div>
<div v-if="file.exp_time">&nbsp;&nbsp; ${ file.exp_time.slice(0,19) }</div>
<div v-else>&nbsp;&nbsp; 永不过期</div>
<div v-if="file.name==='文本分享'">
<span style="white-space: pre-line">&nbsp;&nbsp; ${ file.text }</span>
</div>
<div v-else>
<span>&nbsp;&nbsp; </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>
-576
View File
@@ -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>&nbsp;&nbsp; </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>