重构中,配置存入数据库,第一次智能生成随机后台地址,登录密码

This commit is contained in:
lan
2023-01-16 16:03:25 +08:00
parent c19da62941
commit ee5beb2261
11 changed files with 230 additions and 230 deletions
View File
+76
View File
@@ -0,0 +1,76 @@
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():
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': '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'
)
else:
# 从数据库更新缓存中的setting
await settings.update(await conn.execute(select(Options).filter()))
async def get_session():
async with AsyncSession(engine, expire_on_commit=False) as s:
yield s
+4 -1
View File
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
from fastapi import Header, HTTPException, Request
import settings
from settings import settings
async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None):
@@ -11,6 +11,9 @@ async def admin_required(pwd: Union[str, None] = Header(default=None), request:
if pwd != settings.ADMIN_PASSWORD and not settings.ENABLE_UPLOAD:
raise HTTPException(status_code=403, detail='本站上传功能已关闭,仅管理员可用')
else:
print(settings.ADMIN_PASSWORD)
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="密码错误,请重新登录")
+18 -6
View File
@@ -4,11 +4,19 @@ from datetime import datetime
from pathlib import Path
from typing import BinaryIO
from fastapi import UploadFile
import settings
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 AliyunFileStore:
class AliyunFileStorage:
def __init__(self):
auth = oss2.Auth(settings.KeyId, settings.KeySecret)
self.bucket = oss2.Bucket(auth, settings.OSS_ENDPOINT, settings.BUCKET_NAME)
@@ -21,10 +29,10 @@ class AliyunFileStore:
now = datetime.now()
path = f"FileCodeBox/upload/{now.year}/{now.month}/{now.day}"
text = f"{path}/{f'{key}.{ext}'}"
return text
return f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/{text}"
async def get_filepath(self, text: str):
text = text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/")
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
@@ -66,10 +74,14 @@ class FileSystemStorage:
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()
@@ -122,5 +134,5 @@ class FileSystemStorage:
STORAGE_ENGINE = {
"filesystem": FileSystemStorage,
"aliyunsystem": AliyunFileStore
"aliyunsystem": AliyunFileStorage
}
+4 -4
View File
@@ -3,10 +3,10 @@ import random
import asyncio
from sqlalchemy import or_, select, delete
from sqlalchemy.ext.asyncio.session import AsyncSession
import settings
from database import Codes, engine
from depends import IPRateLimit
from storage import STORAGE_ENGINE
from .database import Codes, engine
from .depends import IPRateLimit
from .storage import STORAGE_ENGINE
from settings import settings
storage = STORAGE_ENGINE[settings.STORAGE_ENGINE]()
-42
View File
@@ -1,42 +0,0 @@
import datetime
from sqlalchemy import Boolean, Column, Integer, String, DateTime, JSON, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.ext.asyncio.session import AsyncSession
import settings
engine = create_async_engine(settings.DATABASE_URL)
Base = declarative_base()
async def init_models():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def get_session():
async with AsyncSession(engine, expire_on_commit=False) as s:
yield s
class Values(Base):
__tablename__ = 'values'
id = Column(Integer, primary_key=True, index=True)
key = Column(String, unique=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, index=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)
+43 -99
View File
@@ -1,29 +1,18 @@
import datetime
import uuid
import asyncio
from pathlib import Path
import os
try:
from fastapi import FastAPI, Depends, UploadFile, Form, File, HTTPException, BackgroundTasks
from sqlalchemy import select, func, update
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.requests import Request
from starlette.responses import HTMLResponse, FileResponse
from starlette.staticfiles import StaticFiles
from sqlalchemy import select, update, func
from sqlalchemy.ext.asyncio.session import AsyncSession
except ImportError:
os.system("pip install -r requirements.txt")
from fastapi import FastAPI, Depends, UploadFile, Form, File, HTTPException, BackgroundTasks
from starlette.requests import Request
from starlette.responses import HTMLResponse, FileResponse
from starlette.staticfiles import StaticFiles
from sqlalchemy import select, update, func
from sqlalchemy.ext.asyncio.session import AsyncSession
import settings
from utils import delete_expire_files, storage, get_code, error_ip_limit, upload_ip_limit
from database import get_session, Codes, init_models, Values
from depends import admin_required
from core.utils import error_ip_limit, upload_ip_limit, get_code, storage
from core.depends import admin_required
from settings import settings
from fastapi import FastAPI, Depends, UploadFile, Form, File, HTTPException, BackgroundTasks
from core.database import init_models, Options, Codes, get_session
# 实例化FastAPI
app = FastAPI(debug=settings.DEBUG, docs_url=None, redoc_url=None)
@@ -34,28 +23,30 @@ if not DATA_ROOT.exists():
DATA_ROOT.mkdir(parents=True)
# 静态文件夹,这个固定就行了,静态资源都放在这里
app.mount(settings.STATIC_URL, StaticFiles(directory='./static'), name="static")
app.mount('/static', StaticFiles(directory='./static'), name="static")
# 首页页面
index_html = open('templates/index.html', 'r', encoding='utf-8').read() \
.replace('{{title}}', settings.TITLE) \
.replace('{{description}}', settings.DESCRIPTION) \
.replace('{{keywords}}', settings.KEYWORDS) \
.replace("'{{fileSizeLimit}}'", str(settings.FILE_SIZE_LIMIT))
# 管理页面
admin_html = open('templates/admin.html', 'r', encoding='utf-8').read() \
.replace('{{title}}', settings.TITLE) \
.replace('{{description}}', settings.DESCRIPTION) \
.replace('{{admin_address}}', settings.ADMIN_ADDRESS) \
.replace('{{keywords}}', settings.KEYWORDS)
@app.on_event('startup')
async def startup():
# 初始化数据库
await init_models()
# 启动后台任务,不定时删除过期文件
asyncio.create_task(delete_expire_files())
# 首页页面
index_html = open('templates/index.html', 'r', encoding='utf-8').read() \
.replace('{{title}}', settings.TITLE) \
.replace('{{description}}', settings.DESCRIPTION) \
.replace('{{keywords}}', settings.KEYWORDS) \
.replace("'{{fileSizeLimit}}'", str(settings.FILE_SIZE_LIMIT)) \
# 管理页面
admin_html = open('templates/admin.html', 'r', encoding='utf-8').read() \
.replace('{{title}}', settings.TITLE) \
.replace('{{description}}', settings.DESCRIPTION) \
.replace('{{keywords}}', settings.KEYWORDS)
@app.get('/')
async def index():
return HTMLResponse(index_html)
@app.get(f'/{settings.ADMIN_ADDRESS}', description='管理页面')
@@ -65,26 +56,19 @@ async def admin():
@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)):
codes = (await s.execute(select(Codes).offset((page - 1) * size).limit(size))).scalars().all()
total = (await s.execute(select(func.count(Codes.id)))).scalar()
return {'detail': '查询成功', 'data': codes, 'paginate': {
return {
'detail': '查询成功',
'data': (await s.execute(select(Codes).offset((page - 1) * size).limit(size))).scalars().all(),
'paginate': {
'page': page,
'size': size,
'total': total
'total': (await s.execute(select(func.count(Codes.id)))).scalar()
}}
@app.patch(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='修改数据库数据')
async def admin_patch(request: Request, s: AsyncSession = Depends(get_session)):
# 从数据库获取系统配置
# 如果不存在config这个key,就创建一个
value = (await s.execute(select(Values).filter(Values.key == 'config'))).scalar_one_or_none()
if not value:
s.add(Values(key='config', value=await request.json()))
else:
await s.execute(update(Values).where(Values.key == 'config').values(value=await request.json()))
await s.commit()
return {'detail': '修改成功'}
# @app.patch(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='修改数据库数据')
# async def admin_patch(request: Request, s: AsyncSession = Depends(get_session)):
# return {'detail': '修改成功'}
@app.delete(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='删除数据库记录')
@@ -103,11 +87,9 @@ async def admin_delete(code: str, s: AsyncSession = Depends(get_session)):
return {'detail': '删除成功'}
@app.get('/config', description='获取系统配置', dependencies=[Depends(admin_required)])
@app.get(f'/{settings.ADMIN_ADDRESS}/config', description='获取系统配置', dependencies=[Depends(admin_required)])
async def config(s: AsyncSession = Depends(get_session)):
# 从数据库获取系统配置
data = (await s.execute(select(Values).filter(Values.key == 'config'))).scalar_one_or_none()
return {'detail': '获取成功', 'data': data.value if data else {'banners': []}}
return {'detail': '获取成功', 'data': (await s.execute(select(Options))).scalars().all()}
@app.get('/')
@@ -127,58 +109,26 @@ async def index(code: str, ip: str = Depends(error_ip_limit), s: AsyncSession =
await s.execute(update(Codes).where(Codes.id == info.id).values(count=info.count - 1))
await s.commit()
if info.type != 'text':
if settings.STORAGE_ENGINE == 'filesystem':
info.text = f'/select?code={code}'
info.text = await storage.get_url(info)
return {
'detail': f'取件成功,文件将在{settings.DELETE_EXPIRE_FILES_INTERVAL}分钟后删除',
'data': {'type': info.type, 'text': info.text, 'name': info.name, 'code': info.code}
}
elif settings.STORAGE_ENGINE == 'aliyunsystem':
info.text = await storage.get_filepath(info.text)
return {
'detail': f'取件成功,链接将在{settings.ACCESSTIME}秒后失效',
'data': {'type': info.type, 'text': info.text, 'name': info.name, 'code': info.code}
}
@app.post('/adminDownloadFile',dependencies=[Depends(admin_required)], description='管理员获取资源链接')
async def admindownloadfile(filetext: str, s: AsyncSession = Depends(get_session)):
if storage.STORAGE_ENGINE == 'aliyunsystem':
filetext = await storage.get_filepath(filetext)
return {
'detail': f'获取文件链接成功,链接将在{settings.ACCESSTIME}秒后失效',
'fileURL': filetext
}
elif storage.STORAGE_ENGINE == 'filesystem':
return {
'detail': f'获取文件链接成功',
'fileURL':filetext
}
@app.post(f'/{settings.ADMIN_ADDRESS}/download', dependencies=[Depends(admin_required)])
async def admin_download_file(filetext: str):
return await storage.get_text(filetext)
@app.get('/banner')
async def banner(request: Request, s: AsyncSession = Depends(get_session)):
async def banner(request: Request):
# 数据库查询config
config = (await s.execute(select(Values).filter(Values.key == 'config'))).scalar_one_or_none()
# 如果存在config,就返回config的value
if config and config.value.get('banners'):
return {
'detail': '查询成功',
'data': config.value['banners'],
'data': settings.BANNERS,
'enable': request.headers.get('pwd', '') == settings.ADMIN_PASSWORD or settings.ENABLE_UPLOAD,
}
# 如果不存在config,就返回默认的banner
return {
'detail': 'banner',
'enable': request.headers.get('pwd', '') == settings.ADMIN_PASSWORD or settings.ENABLE_UPLOAD,
'data': [{
'text': 'FileCodeBox',
'url': 'https://github.com/vastsa/FileCodeBox',
'src': '/static/banners/img_1.png'
}, {
'text': 'FileCodeBox',
'url': 'https://www.lanol.cn',
'src': '/static/banners/img_2.png'
}]
}
@app.get('/select')
@@ -196,10 +146,7 @@ async def get_file(code: str, ip: str = Depends(error_ip_limit), s: AsyncSession
# 如果是文件,返回文件
else:
filepath = await storage.get_filepath(info.text)
if settings.STORAGE_ENGINE == 'filesystem':
return FileResponse(filepath, filename=info.name)
else:
return {'detail': '查询成功', 'data': filepath}
@app.post('/share', dependencies=[Depends(admin_required)], description='分享文件')
@@ -229,10 +176,7 @@ async def share(background_tasks: BackgroundTasks, text: str = Form(default=None
background_tasks.add_task(storage.save_file, file, _text)
else:
size, _text, _type, name = len(text), text, 'text', '文本分享'
if settings.STORAGE_ENGINE == 'aliyunsystem':
_text = f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/"+_text
info = Codes(code=code, text=_text, size=size, type=_type, name=name, count=exp_count, exp_time=exp_time, key=key)
s.add(info)
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 {
+30 -22
View File
@@ -1,15 +1,14 @@
import uuid
from starlette.config import Config
import os
import shutil
# 配置文件.env
# 请将.env移动至data目录,方便docker部署
# 判断根目录下是否存在.env文件
if os.path.exists('.env'):
# 将文件复制到data目录
shutil.copy('.env', 'data/.env')
config = Config("data/.env")
class Settings:
# 项目版本
VERSION: str = config("VERSION", default="1.6")
# 是否开启DEBUG模式
DEBUG = config('DEBUG', cast=bool, default=False)
# 端口
@@ -37,27 +36,36 @@ 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="admin")
ADMIN_ADDRESS = config('ADMIN_ADDRESS', cast=str, default=uuid.uuid4().hex)
# 管理密码
ADMIN_PASSWORD = config('ADMIN_PASSWORD', cast=str, default="admin")
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,文件快递柜,口令传送箱,匿名口令分享文本,文件等文件")
DESCRIPTION = config('DESCRIPTION', cast=str, default="FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件")
# 网站关键词
KEYWORDS = config('KEYWORDS', cast=str, default="FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件等文件")
KEYWORDS = config('KEYWORDS', cast=str, default="FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件")
# 存储引擎:['aliyunsystem','filesystem']
STORAGE_ENGINE = config('STORAGE_ENGINE', cast=str, default="filesystem")
# 如果使用阿里云OSS的话需要创建如下参数
# 阿里云账号AccessKey
KeyId = config('KeyId', cast=str, default="阿里云账号AccessKey")
# 阿里云账号AccessKeySecret
KeySecret = config('KeySecret', cast=str, default="阿里云账号AccessKeySecret")
# 阿里云OSS Bucket的地域节点
OSS_ENDPOINT = config('BUCKET_URL', cast=str, default="阿里云OSS Bucket的地域节点")
# 阿里云OSS Bucket的BucketName
BUCKET_NAME = config('BUCKET_NAME', cast=str, default="阿里云OSS Bucket的BucketName")
# 访问文件的读取时长(s
ACCESSTIME = config('ACCESSTIME', cast=int, default=60)
# 存储引擎配置
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'
}]
async def update(self, options) -> None:
for i, key, value in options:
if hasattr(self, key):
setattr(self, key, value)
settings = Settings()
+1 -2
View File
@@ -126,12 +126,11 @@
if (pwd) {
login = true;
this.pwd = pwd;
axios.get('/config', {
axios.get('{{admin_address}}/config', {
headers: {
pwd: pwd
}
}).then(res => {
console.log(res.data.data)
this.config = res.data.data;
});
this.loginAdmin();
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="description" content="{{description}}"/>
<meta name="keywords" content="{{keywords}}"/>
<meta name="generator" content="FileCodeBox"/>
<meta name="template" content="Lan-V1.5.9.2"/>
<meta name="template" content="Lan-V1.6"/>
<style>
body {
background: #f5f5f5;