Merge pull request #80 from vastsa/new

新版本,前端改vite+vue3
This commit is contained in:
vastsa
2023-08-14 18:00:19 +08:00
committed by GitHub
80 changed files with 4191 additions and 5230 deletions
+3 -1
View File
@@ -80,7 +80,9 @@ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/ .pybuilder/
target/ target/
*.db
./filecodebox.db-shm
./filecodebox.db-wal
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
+4
View File
@@ -0,0 +1,4 @@
# @Time : 2023/8/13 20:43
# @Author : Lan
# @File : __init__.py.py
# @Software: PyCharm
+4
View File
@@ -0,0 +1,4 @@
# @Time : 2023/8/14 14:38
# @Author : Lan
# @File : __init__.py.py
# @Software: PyCharm
+10
View File
@@ -0,0 +1,10 @@
# @Time : 2023/8/14 14:38
# @Author : Lan
# @File : views.py
# @Software: PyCharm
from fastapi import APIRouter
admin_api = APIRouter(
prefix='/admin',
tags=['管理'],
)
+4
View File
@@ -0,0 +1,4 @@
# @Time : 2023/8/13 20:43
# @Author : Lan
# @File : __init__.py.py
# @Software: PyCharm
+7 -10
View File
@@ -1,20 +1,17 @@
# @Time : 2023/8/14 12:20
# @Author : Lan
# @File : depends.py
# @Software: PyCharm
from typing import Union from typing import Union
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import Header, HTTPException, Request from fastapi import Header, HTTPException, Request
from settings import settings from core.response import APIResponse
async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None): async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None):
if 'share' in request.url.path: return False
if pwd != settings.ADMIN_PASSWORD and not settings.ENABLE_UPLOAD:
raise HTTPException(status_code=403, detail='本站上传功能已关闭,仅管理员可用')
else:
if settings.ADMIN_PASSWORD is None:
raise HTTPException(status_code=404, detail='您未设置管理员密码,无法使用此功能,请更新配置文件后,重启系统')
if not pwd or pwd != settings.ADMIN_PASSWORD:
raise HTTPException(status_code=401, detail="密码错误,请重新登录")
class IPRateLimit: class IPRateLimit:
@@ -48,5 +45,5 @@ class IPRateLimit:
def __call__(self, request: Request): def __call__(self, request: Request):
ip = request.headers.get('X-Real-IP', request.headers.get('X-Forwarded-For', request.client.host)) ip = request.headers.get('X-Real-IP', request.headers.get('X-Forwarded-For', request.client.host))
if not self.check_ip(ip): if not self.check_ip(ip):
raise HTTPException(status_code=400, detail=f"请求次数过多,请稍后再试") raise HTTPException(status_code=423, detail=f"请求次数过多,请稍后再试")
return ip return ip
+40
View File
@@ -0,0 +1,40 @@
# @Time : 2023/8/13 20:43
# @Author : Lan
# @File : models.py
# @Software: PyCharm
from datetime import datetime
from typing import Optional
from tortoise import fields
from tortoise.models import Model
from tortoise.contrib.pydantic import pydantic_model_creator
from core.utils import get_now
class FileCodes(Model):
id: Optional[int] = fields.IntField(pk=True)
code: Optional[int] = fields.CharField(description='分享码', max_length=255, index=True, unique=True)
prefix: Optional[str] = fields.CharField(max_length=255, description='前缀', default='')
suffix: Optional[str] = fields.CharField(max_length=255, description='后缀', default='')
uuid_file_name: Optional[str] = fields.CharField(max_length=255, description='uuid文件名', null=True)
file_path: Optional[str] = fields.CharField(max_length=255, description='文件路径', null=True)
size: Optional[int] = fields.IntField(description='文件大小', default=0)
text: Optional[str] = fields.TextField(description='文本内容', null=True)
expired_at: Optional[datetime] = fields.DatetimeField(null=True, description='过期时间')
expired_count: Optional[int] = fields.IntField(description='可用次数', default=0)
used_count: Optional[int] = fields.IntField(description='已用次数', default=0)
created_at: Optional[datetime] = fields.DatetimeField(auto_now_add=True, description='创建时间')
async def is_expired(self):
if self.expired_at and (self.expired_count == -1 or self.used_count < self.expired_count):
return self.expired_at < await get_now()
else:
return self.expired_count != -1 and self.used_count >= self.expired_count
async def get_file_path(self):
return f"{self.file_path}/{self.uuid_file_name}"
file_codes_pydantic = pydantic_model_creator(FileCodes, name='FileCodes')
+5
View File
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class SelectFileModel(BaseModel):
code: str
+78
View File
@@ -0,0 +1,78 @@
# @Time : 2023/8/14 01:10
# @Author : Lan
# @File : utils.py
# @Software: PyCharm
import datetime
import uuid
import os
from fastapi import UploadFile
from apps.base.depends import IPRateLimit
from apps.base.models import FileCodes
from core.utils import get_random_num, get_random_string
async def get_file_path_name(file: UploadFile):
"""
获取文件路径和文件名
:param file:
:return: {
'path': 'share/data/2021/08/13',
'suffix': '.jpg',
'prefix': 'test',
'file_uuid': '44a83bbd70e04c8aa7fd93bfd8c88249',
'uuid_file_name': '44a83bbd70e04c8aa7fd93bfd8c88249.jpg',
'save_path': 'share/data/2021/08/13/44a83bbd70e04c8aa7fd93bfd8c88249.jpg'
}
"""
today = datetime.datetime.now()
path = f"share/data/{today.strftime('%Y/%m/%d')}"
prefix, suffix = os.path.splitext(file.filename)
file_uuid = f"{uuid.uuid4().hex}"
uuid_file_name = f"{file_uuid}{suffix}"
save_path = f"{path}/{uuid_file_name}"
return path, suffix, prefix, uuid_file_name, save_path
async def get_expire_info(expire_value: int, expire_style: str):
"""
获取过期信息
:param expire_value:
:param expire_style:
:return: expired_at 过期时间, expired_count 可用次数, used_count 已用次数, code 随机码
"""
expired_count, used_count, now, code = -1, 0, datetime.datetime.now(), None
if expire_style == 'day':
expired_at = now + datetime.timedelta(days=expire_value)
elif expire_style == 'hour':
expired_at = now + datetime.timedelta(hours=expire_value)
elif expire_style == 'minute':
expired_at = now + datetime.timedelta(minutes=expire_value)
elif expire_style == 'count':
expired_at = now + datetime.timedelta(days=1)
expired_count = expire_value
elif expire_style == 'forever':
expired_at = None
code = await get_random_code(style='string')
else:
expired_at = now + datetime.timedelta(days=1)
if not code:
code = await get_random_code()
return expired_at, expired_count, used_count, code
async def get_random_code(style='num'):
"""
获取随机字符串
:return:
"""
while True:
code = await get_random_num() if style == 'num' else await get_random_string()
if not await FileCodes.filter(code=code).exists():
return code
# 错误IP限制器
error_ip_limit = IPRateLimit(1, 1)
# 上传文件限制器
upload_ip_limit = IPRateLimit(10, 1)
+91
View File
@@ -0,0 +1,91 @@
# @Time : 2023/8/14 03:59
# @Author : Lan
# @File : views.py
# @Software: PyCharm
from fastapi import APIRouter, Form, UploadFile, File, Depends
from starlette.responses import FileResponse
from apps.base.models import FileCodes
from apps.base.pydantics import SelectFileModel
from apps.base.utils import get_expire_info, get_file_path_name, error_ip_limit
from core.response import APIResponse
from core.storage import file_storage
share_api = APIRouter(
prefix='/share',
tags=['分享'],
)
@share_api.post('/text/')
async def share_text(text: str = Form(...), expire_value: int = Form(default=1, gt=0), expire_style: str = Form(default='day')):
expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style)
await FileCodes.create(
code=code,
text=text,
expired_at=expired_at,
expired_count=expired_count,
used_count=used_count,
size=len(text),
prefix='文本分享'
)
return APIResponse(detail={
'code': code,
})
@share_api.post('/file/')
async def share_file(expire_value: int = Form(default=1, gt=0), expire_style: str = Form(default='day'), file: UploadFile = File(...)):
expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style)
path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(file)
await file_storage.save_file(file, save_path)
await FileCodes.create(
code=code,
prefix=prefix,
suffix=suffix,
uuid_file_name=uuid_file_name,
file_path=path,
size=file.size,
expired_at=expired_at,
expired_count=expired_count,
used_count=used_count,
)
return APIResponse(detail={
'code': code,
'name': file.filename,
})
@share_api.post('/select/')
async def select_file(data: SelectFileModel, ip: str = Depends(error_ip_limit)):
file_code = await FileCodes.filter(code=data.code).first()
if not file_code:
error_ip_limit.add_ip(ip)
return APIResponse(code=404, detail='文件不存在')
if await file_code.is_expired():
return APIResponse(code=403, detail='文件已过期')
file_code.used_count += 1
await file_code.save()
return APIResponse(detail={
'code': file_code.code,
'name': file_code.prefix + file_code.suffix,
'size': file_code.size,
'text': file_code.text if file_code.text is not None else await file_storage.get_file_url(file_code),
})
@share_api.get('/download')
async def download_file(key: str, code: str, ip: str = Depends(error_ip_limit)):
is_valid = await file_storage.get_select_token(code) == key
if not is_valid:
error_ip_limit.add_ip(ip)
file_code = await FileCodes.filter(code=code).first()
if not file_code:
return APIResponse(code=404, detail='文件不存在')
if file_code.text:
return APIResponse(detail=file_code.text)
else:
file_path = file_storage.root_path / await file_code.get_file_path()
if not file_path.exists():
return APIResponse(code=404, detail='文件已过期删除')
return FileResponse(file_path, filename=file_code.prefix + file_code.suffix)
+4
View File
@@ -0,0 +1,4 @@
# @Time : 2023/8/11 20:06
# @Author : Lan
# @File : __init__.py.py
# @Software: PyCharm
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
+15
View File
@@ -0,0 +1,15 @@
# @Time : 2023/8/14 11:48
# @Author : Lan
# @File : response.py
# @Software: PyCharm
from typing import Generic, TypeVar
from pydantic.v1.generics import GenericModel
T = TypeVar('T')
class APIResponse(GenericModel, Generic[T]):
code: int = 200
message: str = 'ok'
detail: T
+51 -152
View File
@@ -1,174 +1,73 @@
import os # @Time : 2023/8/11 20:06
# @Author : Lan
# @File : storage.py
# @Software: PyCharm
import asyncio import asyncio
import hashlib
import time import time
import uuid
from datetime import datetime import aioboto3
from pathlib import Path
from typing import BinaryIO
from fastapi import UploadFile from fastapi import UploadFile
from pathlib import Path
from core.database import Codes from apps.base.models import FileCodes
from settings import settings
if settings.STORAGE_ENGINE == 'aliyunsystem':
try:
import oss2
except ImportError:
os.system('pip install oss2')
import oss2
class FileSystemStorage: class SystemFileStorage:
def __init__(self): def __init__(self):
self.DATA_ROOT = Path(settings.DATA_ROOT) self.chunk_size = 256 * 1024
self.STATIC_URL = settings.STATIC_URL self.root_path = Path('./data')
self.NAME = "filesystem" self.token = '123456'
self.DOWN_PATH = '/select'
async def get_filepath(self, text: str): def _save(self, file, save_path):
return self.DATA_ROOT / text.lstrip(self.STATIC_URL + '/') with open(save_path, 'wb') as f:
chunk = file.read(self.chunk_size)
async def get_url(self, info: Codes):
return f'{self.DOWN_PATH}?code={info.code}'
async def get_text(self, file: UploadFile, key: str):
ext = file.filename.split('.')[-1]
now = datetime.now()
path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/"
if not path.exists():
path.mkdir(parents=True)
text = f"{self.STATIC_URL}/{(path / f'{key}.{ext}').relative_to(self.DATA_ROOT)}"
return text
@staticmethod
async def get_size(file: UploadFile):
f = file.file
f.seek(0, os.SEEK_END)
size = f.tell()
f.seek(0, os.SEEK_SET)
return size
@staticmethod
def _save(filepath, file: BinaryIO):
with open(filepath, 'wb') as f:
chunk_size = 256 * 1024
chunk = file.read(chunk_size)
while chunk: while chunk:
f.write(chunk) f.write(chunk)
chunk = file.read(chunk_size) chunk = file.read(self.chunk_size)
@staticmethod async def save_file(self, file: UploadFile, save_path: str):
def _save_chunk(filepath, file: bytes): save_path = self.root_path / save_path
with open(filepath, 'wb') as f: if not save_path.parent.exists():
f.write(file) save_path.parent.mkdir(parents=True)
await asyncio.to_thread(self._save, file.file, save_path)
async def create_upload_file(self): async def delete_file(self, file_code: FileCodes):
file_key = uuid.uuid4().hex save_path = self.root_path / await file_code.get_file_path()
file_path = self.DATA_ROOT / f"temp/{file_key}" if save_path.exists():
if not file_path.exists(): save_path.unlink()
file_path.mkdir(parents=True)
return file_key
async def save_chunk_file(self, file_key, file_chunk, chunk_index, chunk_total): async def get_select_token(self, code):
file_path = self.DATA_ROOT / f"temp/{file_key}/" return hashlib.sha256(f"{code}{int(time.time() / 1000)}000{self.token}".encode()).hexdigest()
await asyncio.to_thread(self._save_chunk, file_path / f"{chunk_total}-{chunk_index}.temp", file_chunk)
async def merge_chunks(self, file_key, file_name, total_chunks: int): async def get_file_url(self, file_code: FileCodes):
ext = file_name.split('.')[-1] return f'/share/download?key={await self.get_select_token(file_code.code)}&code={file_code.code}'
now = datetime.now()
path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/"
if not path.exists():
path.mkdir(parents=True)
text = f"{self.STATIC_URL}/{(path / f'{file_key}.{ext}').relative_to(self.DATA_ROOT)}"
with open(path / f'{file_key}.{ext}', 'wb') as f:
for i in range(1, total_chunks + 1):
now_temp = self.DATA_ROOT / f'temp/{file_key}/{total_chunks}-{i}.temp'
with open(now_temp, 'rb') as r:
f.write(r.read())
await asyncio.to_thread(os.remove, now_temp)
await asyncio.to_thread(os.rmdir, self.DATA_ROOT / f'temp/{file_key}/')
return text
async def save_file(self, file: UploadFile, text: str):
filepath = await self.get_filepath(text)
await asyncio.to_thread(self._save, filepath, file.file)
async def delete_file(self, text: str):
filepath = await self.get_filepath(text)
if filepath.exists():
await asyncio.to_thread(os.remove, filepath)
await asyncio.to_thread(self.judge_delete_folder, filepath)
async def delete_files(self, texts):
tasks = [self.delete_file(text) for text in texts]
await asyncio.gather(*tasks)
def judge_delete_folder(self, filepath):
current = filepath.parent
while current != self.DATA_ROOT:
if not list(current.iterdir()):
os.rmdir(current)
current = current.parent
else:
break
class AliyunFileStorage: class S3FileStorage:
def __init__(self): def __init__(self):
auth = oss2.Auth(settings.KeyId, settings.KeySecret) self.access_key_id = ''
self.bucket = oss2.Bucket(auth, settings.OSS_ENDPOINT, settings.BUCKET_NAME) self.secret_access_key = ''
self.bucket_name = ''
self.endpoint_url = ''
self.session = aioboto3.Session(
aws_access_key_id=self.access_key_id, aws_secret_access_key=self.secret_access_key
)
def upload_file(self, upload_filepath, remote_filepath): async def save_file(self, file: UploadFile, save_path: str):
self.bucket.put_object_from_file(remote_filepath, upload_filepath) async with self.session.client("s3", endpoint_url=self.endpoint_url) as s3:
await s3.put_object(Bucket=self.bucket_name, Key=save_path, Body=await file.read(), ContentType=file.content_type)
async def get_text(self, file: UploadFile, key: str): async def delete_file(self, file_code: FileCodes):
ext = file.filename.split('.')[-1] async with self.session.client("s3", endpoint_url=self.endpoint_url) as s3:
now = datetime.now() await s3.delete_object(Bucket=self.bucket_name, Key=await file_code.get_file_path())
path = f"FileCodeBox/upload/{now.year}/{now.month}/{now.day}"
text = f"{path}/{f'{key}.{ext}'}"
return f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/{text}"
async def get_url(self, info: Codes): async def get_file_url(self, file_code: FileCodes):
text = info.text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/") if file_code.prefix == '文本分享':
url = self.bucket.sign_url('GET', text, settings.ACCESSTIME, slash_safe=True) return file_code.text
return url async with self.session.client("s3", endpoint_url=self.endpoint_url) as s3:
result = await s3.generate_presigned_url('get_object', Params={'Bucket': self.bucket_name, 'Key': await file_code.get_file_path()}, ExpiresIn=3600)
@staticmethod return result
async def get_size(file: UploadFile):
f = file.file
f.seek(0, os.SEEK_END)
size = f.tell()
f.seek(0, os.SEEK_SET)
return size
@staticmethod
def _save(filepath, file: BinaryIO):
with open(filepath, 'wb') as f:
chunk_size = 256 * 1024
chunk = file.read(chunk_size)
while chunk:
f.write(chunk)
chunk = file.read(chunk_size)
async def save_file(self, file: UploadFile, remote_filepath: str):
now = int(datetime.now().timestamp())
upload_filepath = settings.DATA_ROOT + str(now)
await asyncio.to_thread(self._save, upload_filepath, file.file)
self.upload_file(upload_filepath, remote_filepath)
remote_filepath = remote_filepath.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/")
self.upload_file(upload_filepath, remote_filepath)
await asyncio.to_thread(os.remove, upload_filepath)
async def delete_files(self, texts):
tasks = [self.delete_file(text) for text in texts]
await asyncio.gather(*tasks)
async def delete_file(self, text: str):
text = text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/")
self.bucket.delete_object(text)
STORAGE_ENGINE = { file_storage = SystemFileStorage()
"filesystem": FileSystemStorage,
"aliyunsystem": AliyunFileStorage
}
+25 -70
View File
@@ -1,83 +1,38 @@
# @Time : 2023/8/13 19:54
# @Author : Lan
# @File : utils.py
# @Software: PyCharm
import datetime import datetime
import hashlib
import random import random
import asyncio
import string import string
import time
from sqlalchemy import or_, select, delete from apps.base.depends import IPRateLimit
from sqlalchemy.ext.asyncio.session import AsyncSession
from .database import Codes, engine
from .depends import IPRateLimit
from .storage import STORAGE_ENGINE
from settings import settings
storage = STORAGE_ENGINE[settings.STORAGE_ENGINE]()
# 错误IP限制器
error_ip_limit = IPRateLimit(settings.ERROR_COUNT, settings.ERROR_MINUTE)
# 上传文件限制器
upload_ip_limit = IPRateLimit(settings.UPLOAD_COUNT, settings.UPLOAD_MINUTE)
async def delete_expire_files():
while True:
async with AsyncSession(engine, expire_on_commit=False) as s:
await error_ip_limit.remove_expired_ip()
await upload_ip_limit.remove_expired_ip()
query = select(Codes).where(or_(Codes.exp_time < datetime.datetime.now(), Codes.count == 0))
exps = (await s.execute(query)).scalars().all()
files = []
exps_ids = []
for exp in exps:
if exp.type != "text":
files.append(exp.text)
exps_ids.append(exp.id)
await storage.delete_files(files)
query = delete(Codes).where(Codes.id.in_(exps_ids))
await s.execute(query)
await s.commit()
await asyncio.sleep(settings.DELETE_EXPIRE_FILES_INTERVAL * 60)
async def get_random_num(): async def get_random_num():
"""
获取随机数
:return:
"""
return random.randint(10000, 99999) return random.randint(10000, 99999)
r_s = string.ascii_uppercase + string.digits
async def get_random_string(): async def get_random_string():
r_s = string.ascii_letters + string.digits """
return ''.join(random.choice(r_s) for _ in range(5)).upper() 获取随机字符串
:return:
"""
return ''.join(random.choice(r_s) for _ in range(5))
async def get_code(s: AsyncSession, exp_style): async def get_now():
if exp_style == 'forever': """
generate = get_random_string 获取当前时间
else: :return:
generate = get_random_num """
code = await generate() return datetime.datetime.now(
while (await s.execute(select(Codes.id).where(Codes.code == code))).scalar(): datetime.timezone(datetime.timedelta(hours=8))
code = await generate() )
return str(code)
async def get_token(ip, code):
return hashlib.sha256(f"{ip}{code}{int(time.time() / 1000)}000{settings.ADMIN_PASSWORD}".encode()).hexdigest()
async def get_expire_info(expire_style, expire_value, s):
now = datetime.datetime.now()
if expire_value <= 0 or expire_value > 999:
return True, None, None
code = await get_code(s, expire_style)
if expire_style == 'day':
return False, now + datetime.timedelta(days=expire_value), -1, code
elif expire_style == 'hour':
return False, now + datetime.timedelta(hours=expire_value), -1, code
elif expire_style == 'minute':
return False, now + datetime.timedelta(minutes=expire_value), -1, code
elif expire_style == 'forever':
return False, None, -1, code
elif expire_style == 'count':
return False, None, expire_value, code
else:
return True, None, None
+15
View File
@@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
+28
View File
@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
.git
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+9
View File
@@ -0,0 +1,9 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
}
+41
View File
@@ -0,0 +1,41 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
CardTools: typeof import('./src/components/CardTools.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSubmenu: typeof import('element-plus/es')['ElSubmenu']
ElTag: typeof import('element-plus/es')['ElTag']
ElUpload: typeof import('element-plus/es')['ElUpload']
FileBox: typeof import('./src/components/FileBox.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
UploadFile: typeof import('./src/components/UploadFile.vue')['default']
UploadText: typeof import('./src/components/UploadText.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/logo_small.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FileCodeBox</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+41
View File
@@ -0,0 +1,41 @@
{
"name": "fcb-fronted",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.4.0",
"element-plus": "^2.3.9",
"pinia": "^2.1.4",
"qrcode.vue": "^3.4.1",
"sass": "^1.64.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.1",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
"@tsconfig/node18": "^18.2.0",
"@types/node": "^18.17.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.0",
"typescript": "~5.1.6",
"vite": "^4.4.6",
"vue-tsc": "^1.8.6"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>
<RouterView />
</div>
</template>
<style scoped></style>
+86
View File
@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f5f5f5;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #212121;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
html{
background: var(--vt-c-white-soft);
}
html.dark{
background: var(--vt-c-black-soft);
}
body {
color: var(--color-text);
display: flex;
justify-content: center;
align-items: center;
line-height: 1.6;
font-family: Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+55
View File
@@ -0,0 +1,55 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
}
.card {
max-width: 400px;
height: 100%;
background-color: #F8FBFE;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
border-radius: 20px !important;
position: relative;
}
.tools {
display: flex;
z-index: 999;
align-items: center;
}
.box {
display: inline-block;
align-items: center;
width: 1.5rem;
height: 1.5rem;
user-select: none;
cursor: pointer;
padding: 1px;
border-radius: 50%;
}
.circle {
padding: 0 4px;
}
.red {
padding: 3px;
background-color: #ff605c;
}
.yellow {
padding: 3px;
background-color: #ffbd44;
}
.green {
background-color: #00ca4e;
padding: 3px;
}
+39
View File
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import { useRoute, useRouter } from "vue-router";
const router = useRouter()
const route = useRoute()
const isDark = useDark()
const toggleDark = useToggle(isDark)
import { Moon, Sunny, Upload, Back, TakeawayBox } from "@element-plus/icons-vue";
import FileBox from "@/components/FileBox.vue";
import { useFileBoxStore } from "@/stores/fileBox";
const fileBoxStore = useFileBoxStore();
</script>
<template>
<div class="tools">
<div class="circle">
<el-icon size="17" color="#212121" class="red box" @click="router.push({'name':route.name=='home'?'send':'home'})">
<Back v-if="route.name=='send'"></Back>
<Upload v-else></Upload>
</el-icon>
</div>
<div class="circle">
<el-icon size="17" color="#212121" class="yellow box" @click="toggleDark(!isDark)">
<Moon v-if="isDark"></Moon>
<Sunny v-else></Sunny>
</el-icon>
</div>
<div class="circle">
<el-icon size="17" color="#212121" class="green box" @click="fileBoxStore.showFileBox=true">
<TakeawayBox></TakeawayBox>
</el-icon>
</div>
<file-box></file-box>
</div>
</template>
<style scoped lang="scss">
</style>
+86
View File
@@ -0,0 +1,86 @@
<script setup lang="ts">
import { useFileDataStore } from "@/stores/fileData";
import { useFileBoxStore } from "@/stores/fileBox";
const fileStore = useFileDataStore();
const fileBoxStore = useFileBoxStore();
import QrcodeVue from "qrcode.vue";
import { useRoute } from "vue-router";
import { ref } from "vue";
const openUrl = (url: string) => {
if (url.startsWith('/')) {
url = window.location.origin + url;
}
window.open(url);
};
const route = useRoute();
const copyText = (text: any, style = 0) => {
if (style === 1) {
text = window.location.origin + '/#/?code=' + text;
}
const temp: any = document.createElement('textarea');
temp.value = text;
document.body.appendChild(temp);
temp.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
}
document.body.removeChild(temp);
};
</script>
<template>
<el-drawer :append-to-body="true" v-model="fileBoxStore.showFileBox" direction="btt" style="max-width: 1080px;margin: auto;"
size="400">
<template #header>
<h4>文件箱</h4>
</template>
<template #default>
<div v-if="route.name=='home'" style="display: flex;flex-wrap: wrap;justify-content: center">
<el-card v-for="(value,index) in fileStore.receiveData" :key="index" style="margin: 0.5rem">
<template #header>
<div style="display: flex;justify-content: space-between">
<h4 style="width: 6rem;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">{{ value.name }}</h4>
<el-button size="small" type="danger" @click="fileStore.deleteReceiveData(index)">删除</el-button>
</div>
</template>
<div style="width: 200px;">
<div style="display: flex;justify-content: space-between">
<qrcode-vue :value="value.text" :size="100"></qrcode-vue>
<div style="display: flex;flex-direction: column;justify-content: space-around">
<el-tag size="large" style="cursor: pointer" @click="copyText(value.code)">{{ value.code }}</el-tag>
<el-tag v-if="value.name!=='文本分享'" size="large" type="success" style="cursor: pointer" @click="openUrl(value.text);">
点击下载
</el-tag>
<el-tag v-else size="large" type="success" style="cursor: pointer" @click="copyText(value.text);">点击复制</el-tag>
</div>
</div>
</div>
</el-card>
</div>
<div v-else style="display: flex;flex-wrap: wrap;justify-content: center">
<el-card v-for="(value,index) in fileStore.shareData" :key="index" style="margin: 0.5rem">
<template #header>
<div style="display: flex;justify-content: space-between">
<h4 style="width: 6rem;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">{{ value.name }}</h4>
<el-button size="small" type="danger" @click="fileStore.deleteShareData(index)">删除</el-button>
</div>
</template>
<div style="width: 200px;">
<el-progress v-if="value.status!='success'" striped :percentage="value.percentage" :text-inside="true"
:stroke-width="20"></el-progress>
<div style="display: flex;justify-content: space-between">
<qrcode-vue :value="value.text" :size="100"></qrcode-vue>
<div style="display: flex;flex-direction: column;justify-content: space-around">
<el-tag size="large" style="cursor: pointer" @click="copyText(value.code)">{{ value.code }}</el-tag>
<el-tag size="large" type="success" style="cursor: pointer" @click="copyText(value.code,1);">复制链接
</el-tag>
</div>
</div>
</div>
</el-card>
</div>
</template>
</el-drawer>
</template>
+154
View File
@@ -0,0 +1,154 @@
<script setup lang="ts">
import { UploadFilled } from '@element-plus/icons-vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { request } from "@/utils/request";
import { useFileDataStore } from "@/stores/fileData";
import { useFileBoxStore } from "@/stores/fileBox";
const fileBoxStore = useFileBoxStore();
const fileStore = useFileDataStore();
const props = defineProps({
shareData: {
type: Object,
default: () => {
return {
expire_value: 1,
expire_style: 'day',
}
}
}
})
const fileList: any = ref([])
const uploadBox: any = ref(null)
const handleOnChangeFileList = (file: any) => {
fileStore.addShareData({
'name': file.name,
'text': '',
'status': file.status,
'percentage': 0,
'size': file.size,
'type': file.raw.type,
'uid': file.uid,
});
};
const handleHttpRequest = (options: any) => {
fileBoxStore.showFileBox = true;
const formData = new FormData();
formData.append('file', options.file);
formData.append('expire_value', props.shareData.expireValue);
formData.append('expire_style', props.shareData.expireStyle);
request(
{
url: "share/file/",
method: "post",
data: formData,
onUploadProgress: (event: any) => {
const percentage = Math.round((event.loaded * 100) / event.total) || 0;
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
file.percentage = percentage;
fileStore.save();
}
});
}
}
).then((res: any) => {
const data = res.detail;
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
file.status = 'success';
file.text = data.text;
file.code = data.code;
fileStore.save();
}
});
}).catch((err: any) => {
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
file.status = 'fail';
file.text = err.message;
fileStore.save();
}
});
});
};
function pasteLister(event: any) {
const items = event.clipboardData && event.clipboardData.items;
if (items && items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'string') {
if (items[i].type.match(/^text\/plain/)) {
items[i].getAsString(function(str:any) {
console.log(str);
});
}
} else {
const file: any = items[i].getAsFile();
if (file) {
console.log(file);
const uid = Date.now();
file.uid = uid;
fileStore.addShareData({
'name': file.name,
'text': '',
'status': 'ready',
'percentage': 0,
'size': file.size,
'type': file.type,
'uid': uid,
});
handleHttpRequest({
file: file,
})
}
}
}
}
}
onUnmounted(()=>{
// 清除剪切板事件
document.removeEventListener('paste', pasteLister);
})
onMounted(()=>{
document.addEventListener('paste', pasteLister);
})
</script>
<template>
<div>
<el-upload
class="upload-demo"
drag
multiple
:show-file-list="false"
ref="uploadBox"
v-model:file-list="fileList"
:on-change="handleOnChangeFileList"
:http-request="handleHttpRequest"
>
<el-icon class="el-icon--upload">
<upload-filled/>
</el-icon>
<div class="el-upload__text">
将文字文件拖粘贴到此处 <em>点击上传</em>
</div>
<div class="el-upload__text" style="font-size: 10px;">天数&lt;7或限制次数24h后删除</div>
<template #tip>
<div class="el-upload__tip">
</div>
</template>
</el-upload>
</div>
</template>
<style lang="scss">
.el-upload {
border-radius: 20px;
}
.el-upload-dragger {
box-shadow: 3px 3px 0 0 rgba(0, 0, 0, 0.2);
border-radius: 20px;
}
</style>
+66
View File
@@ -0,0 +1,66 @@
<script setup lang="ts">
import { ref } from 'vue'
import { request } from "@/utils/request";
const shareText = ref('')
import { useFileDataStore } from "@/stores/fileData";
import { useFileBoxStore } from "@/stores/fileBox";
const fileBoxStore = useFileBoxStore();
const fileStore = useFileDataStore();
const props = defineProps({
shareData: {
type: Object,
default: () => {
return {
expire_value: 1,
expire_style: 'day',
}
}
}
})
const handleSubmitShareText = ()=>{
if (shareText.value === '') {
alert('请输入您要分享的文本');
} else {
const formData = new FormData();
formData.append('text', shareText.value);
formData.append('expire_value', props.shareData.expireValue);
formData.append('expire_style', props.shareData.expireStyle);
request({
'url': 'share/text/',
'method': 'post',
'data': formData,
}).then((res: any) => {
const data = res.detail;
fileBoxStore.showFileBox = true;
fileStore.addShareData({
'name': '文本分享',
'text': data.text,
'code': data.code,
'status': 'success',
'percentage': 100,
'size': shareText.value.length,
'type': 'text',
'uid': Date.now(),
})
});
}
}
</script>
<template>
<div style="position: relative">
<el-input
placeholder="请输入您要寄出的文本"
v-model="shareText"
type="textarea"
:rows="9"
:input-style="{'border-radius':'20px','border':'1px dashed var(--el-border-color)','box-shadow':'none'}"
>
</el-input>
<el-button @click="handleSubmitShareText" style="position: absolute;right: 0;top: 0;border-radius: 0 20px 0 20px;margin: 1px;background: rgba(255,255,255,0.2)" size="large">分享</el-button>
</div>
</template>
<style scoped lang="scss">
</style>
+13
View File
@@ -0,0 +1,13 @@
import './assets/main.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
+24
View File
@@ -0,0 +1,24 @@
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/send',
name: 'send',
component: () => import('@/views/SendView.vue'),
},
{
path: '/admin',
name: 'admin',
component: () => import('@/views/AdminView.vue'),
}
],
});
export default router;
+7
View File
@@ -0,0 +1,7 @@
import { defineStore } from 'pinia';
import {ref} from "vue";
export const useFileBoxStore = defineStore('fileBox', () => {
const showFileBox = ref(false);
return { showFileBox };
});
+31
View File
@@ -0,0 +1,31 @@
import { defineStore } from 'pinia';
import {reactive} from "vue";
export const useFileDataStore = defineStore('fileData', () => {
const receiveData = reactive(JSON.parse(localStorage.getItem('receiveData')||'[]') || []); // 接收的数据
const shareData = reactive(JSON.parse(localStorage.getItem('shareData')||'[]') || []); // 接收的数据
function save() {
localStorage.setItem('receiveData', JSON.stringify(receiveData));
localStorage.setItem('shareData', JSON.stringify(shareData));
}
function addReceiveData(data:any) {
receiveData.push(data);
save();
}
function addShareData(data:any) {
shareData.push(data);
save();
}
function deleteReceiveData(index: number) {
receiveData.splice(index, 1);
save();
}
function deleteShareData(index: number) {
shareData.splice(index, 1);
save();
}
return { receiveData, shareData, save, addShareData, addReceiveData, deleteReceiveData, deleteShareData };
});
+25
View File
@@ -0,0 +1,25 @@
// @ts-ignore
import axios from "axios";
const instance = axios.create({
baseURL: "http://localhost:12345",
timeout: 6000000,
headers:{
'Authorization':localStorage.getItem('auth')
}
});
// 对响应进行拦截
instance.interceptors.response.use(
(response:any) => {
if (response.data.code === 200) {
return response.data;
} else {
alert(response.data.detail);
return Promise.reject(response.data);
}
}, (error:any) => {
alert(error.response.data.detail);
return Promise.reject(error);
});
export const request = instance;
+14
View File
@@ -0,0 +1,14 @@
<template>
<el-container style="width: 100vw;height: 100%;position: relative">
<el-aside width="200" style="height: 100%">
<el-menu>
<el-menu-item index="1">文件管理</el-menu-item>
<el-menu-item index="4">1</el-menu-item>
</el-menu>
</el-aside>
<el-main>Main</el-main>
</el-container>
</template>
<script setup lang="ts">
</script>
+110
View File
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { Upload, TakeawayBox } from '@element-plus/icons-vue';
import { onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from "vue-router";
import CardTools from "@/components/CardTools.vue";
import { useFileBoxStore } from "@/stores/fileBox";
import { useFileDataStore } from "@/stores/fileData";
import { request } from "@/utils/request";
const fileBoxStore = useFileBoxStore();
const fileStore = useFileDataStore();
const router = useRouter()
const route = useRoute()
const code = ref('')
const input_status = reactive({
'readonly': false,
'loading': false,
})
onMounted(() => {
const query_code = route.query.code as string;
if (query_code) {
code.value = query_code
}
})
watch(code, (newVal) => {
if (newVal.length === 5) {
input_status.readonly = true;
input_status.loading = true;
request({
'url': '/share/select',
'method': 'POST',
'data': {
'code': newVal
}
}).then((res: any) => {
fileBoxStore.showFileBox = true;
let flag = true;
fileStore.receiveData.forEach((file: any) => {
if (file.code === res.detail.code) {
flag = false;
return;
}
});
if (flag) {
fileStore.addReceiveData(res.detail);
}
}).finally(() => {
input_status.readonly = false;
input_status.loading = false;
code.value = '';
});
}
});
const listenInput = (num: number) => {
if (code.value.length < 5) {
code.value += num
}
};
</script>
<template>
<main>
<el-card class="card" style="padding-bottom: 1rem">
<CardTools/>
<el-row style="text-align: center">
<el-col :span="24">
<el-input :readonly="input_status.readonly" v-loading="input_status.loading" v-model="code" class="code-input" round autofocus clearable maxlength="5" placeholder="请输入五位取件码"/>
</el-col>
<el-col :span=8 v-for="i in 9" :key="i">
<el-button class="key-button" round @click="listenInput(i)">{{ i }}</el-button>
</el-col>
<el-col :span=8>
<el-button @click="router.push({'name':'send'})" class="key-button" :icon="Upload" round>
</el-button>
</el-col>
<el-col :span=8>
<el-button class="key-button" round @click="listenInput(0)">0</el-button>
</el-col>
<el-col :span=8>
<el-button class="key-button" round :icon="TakeawayBox" @click="fileBoxStore.showFileBox=true">
</el-button>
</el-col>
</el-row>
</el-card>
<div style="text-align: center; margin-top: 1rem;color: #606266">
<a style="text-decoration: none;color: #606266" target="_blank" href="https://github.com/vastsa/FileCodeBox">FileCodeBox V2.0 Beta</a>
</div>
</main>
</template>
<style lang='scss'>
.key-button{
width: 6rem;
height: 6rem;
margin: 0.2rem;
font-size: 2rem;
font-weight: bold;
text-align: center;
}
.code-input {
height: 100px;
font-size: 30px;
font-weight: bold;
margin: 1rem 0;
.el-input__wrapper{
border-radius: 20px !important;
}
}
</style>
+53
View File
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ref } from 'vue'
import CardTools from "@/components/CardTools.vue";
import UploadFile from "@/components/UploadFile.vue";
import UploadText from "@/components/UploadText.vue";
const shareData = ref({
expireValue: 1,
expireStyle: 'day',
targetType: 'file',
})
</script>
<template>
<main>
<el-card class="card" style="padding: 1rem;position: relative" :body-style="{ padding: '0px 0px 20px 0px' }">
<card-tools/>
<div style="display: flex;margin-top: 1rem">
<div>
<el-input
v-model="shareData.expireValue"
style="width: 200px"
placeholder="请输入值"
>
<template #prepend>
<el-select v-model="shareData.expireStyle" placeholder="过期方式" style="width: 75px">
<el-option label="天数" value="day" />
<el-option label="小时" value="hour" />
<el-option label="分钟" value="minute" />
<el-option label="永久" value="forever" />
<el-option label="次数" value="count" />
</el-select>
</template>
<template #append>
<span v-if="shareData.expireStyle=='day'"></span>
<span v-else-if="shareData.expireStyle=='hour'"></span>
<span v-else-if="shareData.expireStyle=='minute'"></span>
<span v-else-if="shareData.expireStyle=='forever'">👌</span>
<span v-else-if="shareData.expireStyle=='count'"></span>
</template>
</el-input>
</div>
<el-radio-group v-model="shareData.targetType" style="margin-left: 1rem;">
<el-radio label="file">文件</el-radio>
<el-radio label="text">文本</el-radio>
</el-radio-group>
</div>
<div style="margin-top: 1rem">
<upload-file :shareData="shareData" v-if="shareData.targetType=='file'"/>
<upload-text :shareData="shareData" v-else-if="shareData.targetType=='text'"/>
</div>
</el-card>
</main>
</template>
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}
+25
View File
@@ -0,0 +1,25 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
File diff suppressed because it is too large Load Diff
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

+40 -237
View File
@@ -1,247 +1,50 @@
import asyncio # @Time : 2023/8/9 23:23
import datetime # @Author : Lan
import uuid # @File : main.py
from pathlib import Path # @Software: PyCharm
from fastapi import FastAPI
from pydantic import BaseModel from starlette.middleware.cors import CORSMiddleware
from sqlalchemy import select, func, update from starlette.responses import HTMLResponse
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.requests import Request
from starlette.responses import HTMLResponse, FileResponse, RedirectResponse
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
from core.utils import error_ip_limit, upload_ip_limit, storage, delete_expire_files, get_token, \ from tortoise.contrib.fastapi import register_tortoise
get_expire_info from apps.base.views import share_api
from core.depends import admin_required from apps.admin.views import admin_api
from fastapi import FastAPI, Depends, Form, File, HTTPException
from core.database import init_models, Options, Codes, get_session
from settings import settings
# 实例化FastAPI app = FastAPI()
app = FastAPI(debug=settings.DEBUG, redoc_url=None, )
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.on_event('startup') app.mount('/assets', StaticFiles(directory='./fcb-fronted/dist/assets'), name="assets")
async def startup(s: AsyncSession = Depends(get_session)):
# 初始化数据库
await init_models(s)
# 启动后台任务,不定时删除过期文件
asyncio.create_task(delete_expire_files())
register_tortoise(
app,
generate_schemas=True,
add_exception_handlers=True,
config={
'connections': {
'default': 'sqlite://filecodebox.db'
},
'apps': {
'models': {
"models": ["apps.base.models"],
'default_connection': 'default',
}
},
"use_tz": False,
"timezone": "Asia/Shanghai",
}
)
# 数据存储文件夹 app.include_router(share_api)
DATA_ROOT = Path(settings.DATA_ROOT) app.include_router(admin_api)
if not DATA_ROOT.exists():
DATA_ROOT.mkdir(parents=True)
# 静态文件夹,这个固定就行了,静态资源都放在这里
app.mount('/static', StaticFiles(directory='./static'), name="static")
# 首页页面
index_html = open('templates/index.html', 'r', encoding='utf-8').read()
# 管理页面
admin_html = open('templates/admin.html', 'r', encoding='utf-8').read()
@app.get('/') @app.get('/')
async def index(): async def index():
return HTMLResponse( return HTMLResponse(content=open('./fcb-fronted/dist/index.html', 'r', encoding='utf-8').read(), status_code=200)
index_html
.replace('{{title}}', settings.TITLE)
.replace('{{description}}', settings.DESCRIPTION)
.replace('{{keywords}}', settings.KEYWORDS)
.replace("'{{fileSizeLimit}}'", str(settings.FILE_SIZE_LIMIT))
)
@app.get(f'/{settings.ADMIN_ADDRESS}', description='管理页面')
async def admin():
return HTMLResponse(
admin_html
.replace('{{title}}', settings.TITLE)
.replace('{{description}}', settings.DESCRIPTION)
.replace('{{admin_address}}', settings.ADMIN_ADDRESS)
.replace('{{keywords}}', settings.KEYWORDS)
)
@app.post(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='查询数据库列表')
async def admin_post(page: int = Form(default=1), size: int = Form(default=10), s: AsyncSession = Depends(get_session)):
infos = (await s.execute(select(Codes).offset((page - 1) * size).limit(size))).scalars().all()
data = [{
'id': info.id,
'code': info.code,
'name': info.name,
'exp_time': info.exp_time,
'count': info.count,
'text': info.text if info.type == 'text' else await storage.get_url(info),
} for info in infos]
return {
'detail': '查询成功',
'data': data,
'paginate': {
'page': page,
'size': size,
'total': (await s.execute(select(func.count(Codes.id)))).scalar()
}}
@app.delete(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='删除数据库记录')
async def admin_delete(code: str, s: AsyncSession = Depends(get_session)):
# 找到相应记录
query = select(Codes).where(Codes.code == code)
# 找到第一条记录
file = (await s.execute(query)).scalars().first()
# 如果记录存在,并且不是文本
if file and file.type != 'text':
# 删除文件
await storage.delete_file(file.text)
# 删除数据库记录
await s.delete(file)
await s.commit()
return {'detail': '删除成功'}
@app.get(f'/{settings.ADMIN_ADDRESS}/config', description='获取系统配置', dependencies=[Depends(admin_required)])
async def config(s: AsyncSession = Depends(get_session)):
# 查询数据库
data = {}
for i in (await s.execute(select(Options))).scalars().all():
data[i.key] = i.value
return {'detail': '获取成功', 'data': data, 'menus': [
{'key': 'INSTALL', 'name': '版本信息'},
{'key': 'WEBSITE', 'name': '网站设置'},
{'key': 'SHARE', 'name': '分享设置'},
{'key': 'BANNERS', 'name': 'Banner'},
]}
@app.patch(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends()], description='修改数据库数据')
async def admin_patch(request: Request, s: AsyncSession = Depends(get_session)):
data = await request.json()
data.pop('INSTALL')
for key, value in data.items():
await s.execute(update(Options).where(Options.key == key).values(value=value))
await settings.update(key, value)
await s.commit()
await settings.updates([[i.id, i.key, i.value] for i in (await s.execute(select(Options))).scalars().all()])
return {'detail': '修改成功'}
@app.post('/')
async def index(code: str, ip: str = Depends(error_ip_limit), s: AsyncSession = Depends(get_session)):
query = select(Codes).where(Codes.code == code)
info = (await s.execute(query)).scalars().first()
if not info:
error_count = settings.ERROR_COUNT - error_ip_limit.add_ip(ip)
raise HTTPException(status_code=404, detail=f"取件码错误,{error_count}次后将被禁止{settings.ERROR_MINUTE}分钟")
if (info.exp_time and info.exp_time < datetime.datetime.now()) or info.count == 0:
raise HTTPException(status_code=404, detail="取件码已失效,请联系寄件人")
await s.execute(update(Codes).where(Codes.id == info.id).values(count=info.count - 1))
await s.commit()
if info.type != 'text':
info.text = f'/select?code={info.code}&token={await get_token(code, ip)}'
return {
'detail': f'取件成功,请立即下载,避免失效!',
'data': {'type': info.type, 'text': info.text, 'name': info.name, 'code': info.code}
}
@app.get('/banner')
async def banner(request: Request):
return {
'detail': '查询成功',
'data': settings.BANNERS,
'enable': request.headers.get('pwd', '') == settings.ADMIN_PASSWORD or settings.ENABLE_UPLOAD,
}
@app.get('/select')
async def get_file(code: str, token: str, ip: str = Depends(error_ip_limit), s: AsyncSession = Depends(get_session)):
# 验证token
if token != await get_token(code, ip):
error_ip_limit.add_ip(ip)
raise HTTPException(status_code=403, detail="口令错误,或已过期,次数过多将被禁止访问")
# 查出数据库记录
query = select(Codes).where(Codes.code == code)
info = (await s.execute(query)).scalars().first()
# 如果记录不存在,IP错误次数+1
if not info:
error_ip_limit.add_ip(ip)
raise HTTPException(status_code=404, detail="口令不存在,次数过多将被禁止访问")
# 如果是文本,直接返回
if info.type == 'text':
return {'detail': '查询成功', 'data': info.text}
# 如果是文件,返回文件
elif storage.NAME != 'filesystem':
# 重定向到文件存储服务器
return RedirectResponse(await storage.get_url(info))
else:
filepath = await storage.get_filepath(info.text)
return FileResponse(filepath, filename=info.name)
@app.post('/file/create/')
async def create_file():
# 生成随机字符串
return {'code': 200, 'data': await storage.create_upload_file()}
@app.post('/file/upload/{file_key}/')
async def upload_file(file_key: str, file: bytes = File(...), chunk_index: int = Form(...),
total_chunks: int = Form(...)):
await storage.save_chunk_file(file_key, file, chunk_index, total_chunks)
return {'code': 200}
@app.get('/file/merge/{file_key}/')
async def merge_chunks(file_key: str, file_name: str, total_chunks: int):
return {'code': 200, 'data': await storage.merge_chunks(file_key, file_name, total_chunks)}
class ShareDataModel(BaseModel):
text: str
size: int = 0
exp_style: str
exp_value: int
type: str
name: str
key: str = uuid.uuid4().hex
@app.post('/share/file/', dependencies=[Depends(admin_required)], description='分享文件')
async def share_file(file_model: ShareDataModel, s: AsyncSession = Depends(get_session),
ip: str = Depends(error_ip_limit)):
exp_error, exp_time, exp_count, code = await get_expire_info(file_model.exp_style, file_model.exp_value, s)
if exp_error:
raise HTTPException(status_code=400, detail='过期值异常')
s.add(Codes(code=code, text=file_model.text, size=file_model.size, type=file_model.type, name=file_model.name,
count=exp_count, exp_time=exp_time, key=file_model.key))
await s.commit()
upload_ip_limit.add_ip(ip)
return {
'detail': '分享成功,请点击我的文件按钮查看上传列表',
'data': {'code': code, 'key': file_model.key, 'name': file_model.name}
}
@app.post('/share/text/', dependencies=[Depends(admin_required)])
async def share_text(text_model: ShareDataModel, s: AsyncSession = Depends(get_session),
ip: str = Depends(error_ip_limit)):
exp_error, exp_time, exp_count, code = await get_expire_info(text_model.exp_style, text_model.exp_value, s, )
if exp_error:
raise HTTPException(status_code=400, detail='过期值异常')
exp_status, exp_time, exp_count, code = await get_expire_info(text_model.exp_style, text_model.exp_value, s)
size, _text, _type, name, key = len(text_model.text), text_model.text, 'text', '文本分享', text_model.key
s.add(Codes(code=code, text=_text, size=size, type=_type, name=name, count=exp_count, exp_time=exp_time, key=key))
await s.commit()
upload_ip_limit.add_ip(ip)
return {
'detail': '分享成功,请点击我的文件按钮查看上传列表',
'data': {'code': code, 'key': key, 'name': name}
}
if __name__ == '__main__':
import uvicorn
uvicorn.run('main:app', host='0.0.0.0', port=settings.PORT, reload=settings.DEBUG)
-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>