This commit is contained in:
lan
2022-12-13 11:28:55 +08:00
parent 047f37038a
commit 412bde5cb4
3 changed files with 0 additions and 608 deletions
-179
View File
@@ -1,179 +0,0 @@
import datetime
import uuid
import random
import asyncio
from pathlib import Path
from fastapi import FastAPI, Depends, UploadFile, Form, File, HTTPException
from starlette.responses import HTMLResponse, FileResponse
from starlette.staticfiles import StaticFiles
from sqlalchemy import or_, select, update, delete
from sqlalchemy.ext.asyncio.session import AsyncSession
import settings
from database import get_session, Codes, init_models, engine
from storage import STORAGE_ENGINE
from depends import admin_required, IPRateLimit
app = FastAPI(debug=settings.DEBUG)
DATA_ROOT = Path(settings.DATA_ROOT)
if not DATA_ROOT.exists():
DATA_ROOT.mkdir(parents=True)
STATIC_URL = settings.STATIC_URL
app.mount(STATIC_URL, StaticFiles(directory=DATA_ROOT), name="static")
storage = STORAGE_ENGINE[settings.STORAGE_ENGINE]()
@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)
admin_html = open('templates/admin.html', 'r', encoding='utf-8').read() \
.replace('{{title}}', settings.TITLE) \
.replace('{{description}}', settings.DESCRIPTION) \
.replace('{{keywords}}', settings.KEYWORDS)
ip_limit = IPRateLimit()
async def delete_expire_files():
while True:
async with AsyncSession(engine, expire_on_commit=False) as s:
query = select(Codes).where(or_(Codes.exp_time < datetime.datetime.now(), Codes.count == 0))
exps = (await s.execute(query)).scalars().all()
files = [{'type': old.type, 'text': old.text} for old in exps]
await storage.delete_files(files)
exps_ids = [exp.id for exp in exps]
query = delete(Codes).where(Codes.id.in_(exps_ids))
await s.execute(query)
await s.commit()
await asyncio.sleep(random.randint(60, 300))
async def get_code(s: AsyncSession):
code = random.randint(10000, 99999)
while (await s.execute(select(Codes.id).where(Codes.code == code))).scalar():
code = random.randint(10000, 99999)
return str(code)
@app.get(f'/{settings.ADMIN_ADDRESS}')
async def admin():
return HTMLResponse(admin_html)
@app.post(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)])
async def admin_post(s: AsyncSession = Depends(get_session)):
query = select(Codes)
codes = (await s.execute(query)).scalars().all()
return {'detail': '查询成功', 'data': codes}
@app.delete(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)])
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()
await storage.delete_file({'type': file.type, 'text': file.text})
await s.delete(file)
await s.commit()
return {'detail': '删除成功'}
@app.get('/')
async def index():
return HTMLResponse(index_html)
@app.get('/select')
async def get_file(code: str, s: AsyncSession = Depends(get_session)):
query = select(Codes).where(Codes.code == code)
info = (await s.execute(query)).scalars().first()
if not info:
raise HTTPException(status_code=404, detail="口令不存在")
if info.type == 'text':
return {'detail': '查询成功', 'data': info.text}
else:
filepath = await storage.get_filepath(info.text)
return FileResponse(filepath, filename=info.name)
@app.post('/')
async def index(code: str, ip: str = Depends(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 - ip_limit.add_ip(ip)
raise HTTPException(status_code=404, detail=f"取件码错误,错误{error_count}次将被禁止10分钟")
if info.exp_time < datetime.datetime.now() or info.count == 0:
await storage.delete_file({'type': info.type, 'text': info.text})
await s.delete(info)
await s.commit()
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={code}'
return {
'detail': '取件成功,请点击""查看',
'data': {'type': info.type, 'text': info.text, 'name': info.name, 'code': info.code}
}
@app.post('/share')
async def share(text: str = Form(default=None), style: str = Form(default='2'), value: int = Form(default=1),
file: UploadFile = File(default=None), s: AsyncSession = Depends(get_session)):
code = await get_code(s)
if style == '2':
if value > 7:
raise HTTPException(status_code=400, detail="最大有效天数为7天")
exp_time = datetime.datetime.now() + datetime.timedelta(days=value)
exp_count = -1
elif style == '1':
if value < 1:
raise HTTPException(status_code=400, detail="最小有效次数为1次")
exp_time = datetime.datetime.now() + datetime.timedelta(days=1)
exp_count = value
else:
exp_time = datetime.datetime.now() + datetime.timedelta(days=1)
exp_count = -1
key = uuid.uuid4().hex
if file:
file_bytes = await file.read()
size = len(file_bytes)
if size > settings.FILE_SIZE_LIMIT:
raise HTTPException(status_code=400, detail="文件过大")
_text, _type, name = await storage.save_file(file, file_bytes, key), file.content_type, file.filename
else:
size, _text, _type, name = len(text), text, '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)
await s.commit()
return {
'detail': '分享成功,请点击文件箱查看取件码',
'data': {'code': code, 'key': key, 'name': name, 'text': _text}
}
if __name__ == '__main__':
import uvicorn
uvicorn.run('main:app', host='0.0.0.0', port=settings.PORT, debug=settings.DEBUG)
-111
View File
@@ -1,111 +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/assert/index.css">
<link rel="shortcut icon" href="/static/assert/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/assert/vue.min.js"></script>
<script src="/static/assert/index.js"></script>
</head>
<body>
<div id="app">
<el-row v-if="login" :gutter="10">
<el-col :xs="0" :sm="4" :md="4" :lg="4" :xl="4">
&nbsp;
</el-col>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
<el-card>
<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>&nbsp;&nbsp; ${ file.exp_time.slice(0,19) }</div>
<div v-if="file.name==='分享文本'">
<span>&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-col>
<el-col :xs="0" :sm="4" :md="4" :lg="4" :xl="4">&nbsp;</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/assert/axios.min.js"></script>
<script>
new Vue({
el: '#app',
delimiters: ['${', '}'],
data: function () {
return {
login: false,
pwd: '',
files: [],
config: {
error_count: 0,
file_size: 0,
admin_pwd: 'admin'
}
};
},
mounted: function () {
const pwd = localStorage.getItem('pwd');
if (pwd) {
login = true;
this.pwd = pwd;
this.loginAdmin();
}
},
methods: {
loginAdmin: function () {
axios.post('', {}, {'headers': {'pwd': this.pwd}}).then(res => {
this.files = res.data.data;
this.login = true;
localStorage.setItem('pwd', this.pwd);
}).catch(e => {
localStorage.clear()
this.$message({'message': e.response.data.detail, 'type': 'error'});
});
},
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'
});
})
},
}
})
</script>
</html>
-318
View File
@@ -1,318 +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/assert/index.css">
<link rel="shortcut icon" href="/static/assert/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-V1.5.1"/>
<style>
.qu .el-button {
width: 100px;
height: 100px;
margin: 0.2rem;
font-size: 30px;
font-weight: bold;
}
.qu .el-input__inner {
height: 100px;
margin: 1rem 0;
font-size: 30px;
font-weight: bold;
}
</style>
<script src="/static/assert/vue.min.js"></script>
<script src="/static/assert/index.js"></script>
</head>
<body>
<div id="app" style="text-align: center">
<el-row v-if="pageNum === 0" class="qu" style="width:400px;margin: 6vh auto 0 auto">
<el-card style="padding-bottom: 1rem">
<el-col :span="24">
<el-input autofocus @input="listenInput" clearable v-model:value="code" maxlength="5"
:disabled="inputDisable" placeholder="请输入五位取件码">
</el-input>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('1')">1</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('2')">2</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('3')">3</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('4')">4</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('5')">5</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('6')">6</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('7')">7</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('8')">8</el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('9')">9</el-button>
</el-col>
<el-col :span=8>
<el-button @click="pageNum=1"></el-button>
</el-col>
<el-col :span=8>
<el-button @click="listenInput('0')">0</el-button>
</el-col>
<el-col :span=8>
<el-button @click="quDrawer=true"></el-button>
</el-col>
</el-card>
</el-row>
<el-row v-else style="width:400px;margin: 20vh auto 0 auto">
<el-col :span="24" style="margin: 1rem 0">
<el-card>
<el-input style="width: 200px" placeholder="数量" v-model="uploadData.value"
class="input-with-select">
<el-select style="width: 75px" v-model="uploadData.style" slot="prepend" placeholder="请选择">
<el-option label="天数" value="2"></el-option>
<el-option label="次数" value="1"></el-option>
</el-select>
<el-button v-if="uploadData.style === '1'" slot="append" disabled></el-button>
<el-button v-else slot="append" disabled></el-button>
</el-input>
<el-radio-group v-model="uploadData.type" style="margin-left: 18px">
<el-radio label="1">文件</el-radio>
<el-radio label="2">文本</el-radio>
</el-radio-group>
<el-upload
v-if="uploadData.type === '1'"
drag
action="/share"
multiple
style="margin: 1rem 0;"
:data="uploadData"
: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 @click="pageNum=0">去取件</el-button>
<el-button @click="cunDrawer=true">取件码</el-button>
<el-button v-if="uploadData.type === '2'" @click="toUploadData">寄出去</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 files" :key="index" class="box-card">
<div style="cursor: pointer;text-align: left">
<div>取件码${ file.code }</div>
<div>文件名${ file.name }</div>
<div v-if="file.name==='文本分享'">
<span>&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-card>
<el-empty v-if="files.length===0" description="请输入取件码,收取文件"></el-empty>
</el-drawer>
<el-drawer
title="文件箱"
:visible.sync="jiDrawer"
:direction="direction"
size="50%">
<el-card>
<el-empty v-if="files.length===0" description="请上传文件"></el-empty>
<el-card style="margin-top: 0.2rem" v-for="(file,index) in files" :key="index">
<el-row>
<el-col :span="20">
<el-row>
<el-col :span="24" style="line-height: 45px">
${ file.name }
</el-col>
<el-col :span="24">
取件码<h1 style="margin: 0;display: inline">${ 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-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</a>
</div>
</div>
</body>
<script src="/static/assert/axios.min.js"></script>
<script>
new Vue({
el: '#app',
delimiters: ['${', '}'],
data: function () {
return {
code: '',
quDrawer: false,
jiDrawer: false,
direction: 'btt',
files: [],
pageNum: 0,
inputDisable: false,
uploadData: {
style: '2',
type: '1',
value: 1,
file: null,
text: ''
},
};
},
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.toUploadData();
});
}
} else {
const file = items[i].getAsFile();
that.$message('剪切板文件正在上传,请稍等');
that.uploadData.file = file
that.toUploadData();
}
}
}
}
});
},
methods: {
http: function (method, url, data = {}, config = {}) {
return new Promise((resolve, reject) => {
axios({
method: method,
url: url,
data: data,
...config
}).then(res => {
this.$message({
message: res.data.detail,
type: 'success'
});
resolve(res.data)
}).catch(err => {
this.$message({
message: err.response.data.detail,
type: 'error'
});
})
})
},
getFile: function () {
const that = this;
this.http('post', `?code=${that.code}`).then(res => {
that.quDrawer = true;
that.files.unshift(res.data);
})
that.code = '';
that.input_disable = false
},
qrcodeUrl(file) {
return 'https://api.qrserver.com/v1/create-qr-code/?data=' + window.location.origin + '/?code=' + file.code
},
listenInput: function (value) {
if (this.code.length < 5) {
this.code += value;
}
if (this.code.length === 5) {
this.inout_disable = true;
this.getFile();
}
},
toUploadData: function () {
if (this.uploadData.text === '' && this.uploadData.file === null) {
this.$message({
message: '请先粘贴文字或者上传文件',
type: 'error'
});
} else {
this.http('post', '/share', this.uploadData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(res => {
this.files.unshift(res.data);
this.jiDrawer = true;
this.uploadData.text = '';
this.uploadData.file = null;
});
}
},
successUpload(response, file, fileList) {
this.$message({
message: response.msg,
type: 'success'
});
this.files.unshift(response.data);
},
errorUpload(error, file, fileList) {
error = JSON.parse(error.message)
this.$message({
message: error.detail,
type: 'error'
});
},
}
})
</script>
</html>