495 lines
21 KiB
Python

#!/bin/python
#coding: utf-8
# +-------------------------------------------------------------------
# | system: 如意面板
# +-------------------------------------------------------------------
# | Author: lybbn
# +-------------------------------------------------------------------
# | QQ: 1042594286
# +-------------------------------------------------------------------
# | Date: 2024-02-29
# +-------------------------------------------------------------------
# | EditDate: 2024-02-29
# +-------------------------------------------------------------------
# ------------------------------
# 文件管理
# ------------------------------
import os,re
import time
from math import ceil
from rest_framework.views import APIView
from utils.jsonResponse import SuccessResponse,ErrorResponse,DetailResponse
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.permissions import IsAuthenticated
from utils.permission import CustomPermission
from utils.common import get_parameter_dic,WriteFile,ast_convert,RunCommand
from utils.security.files import list_files_in_directory,get_directory_size,delete_file,delete_dir,create_file,create_dir,rename_file,copy_file,copy_dir,move_file
from utils.security.files import get_filedir_attribute,batch_operate,get_filename_ext,auto_detect_file_language
from utils.security.no_delete_list import check_in_black_list
from utils.common import ly_md5 as md5
import platform
from django.http import FileResponse,StreamingHttpResponse
from django.utils.encoding import escape_uri_path
import mimetypes
from utils.streamingmedia_response import stream_video
from django.conf import settings
from django.http import HttpResponse
is_windows = True if platform.system() == 'Windows' else False
def getIndexPath():
if is_windows:
return "c:/"
else:
return "/tmp"
#返回nginx 404 错误页,降低信息泄露风险,提升安全性
def ResponseNginx404(state = 404):
html_content = '''<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.21.1</center>
</body>
</html>'''
return HttpResponse(html_content, content_type='text/html',status=state,charset='utf-8')
def get_type_name(type):
c_type_name = ""
if type == "copy":
c_type_name = "复制"
elif type == "move":
c_type_name = "移动"
elif type == "zip":
c_type_name = "压缩"
elif type == "unzip":
c_type_name = "解压"
return c_type_name
class RYFileManageView(APIView):
"""
get:
文件管理
post:
文件管理
"""
permission_classes = [IsAuthenticated,CustomPermission]
authentication_classes = [JWTAuthentication]
def post(self, request):
reqData = get_parameter_dic(request)
action = reqData.get("action","")
is_windows = True if platform.system() == 'Windows' else False
#路径处理
path = reqData.get("path",getIndexPath())
if path == "default":
path = getIndexPath()
if not path:#根目录
if is_windows:
path = ""
else:
path = "/"
if is_windows:#windows 路径处理
path = path.replace("\\", "/")
#接口逻辑处理
if action == "list_dir":
containSub = reqData.get("containSub",False)
isDir = reqData.get("isDir",False)#只显示目录
search = reqData.get("search","")
if search:
search = search.strip().lower()
order = reqData.get("order","")
sort = reqData.get("sort","name")
is_reverse = True if (order and order == "desc") else False
data_info = list_files_in_directory(dst_path=path,sort=sort,is_windows=is_windows,is_reverse=is_reverse,search=search,containSub=containSub,isDir=isDir)
data_dirs = data_info.get('data',[])
page = int(reqData.get("page",1))
limit = int(reqData.get("limit",100))
#一次最大条数限制
limit = 3000 if limit > 3000 else limit
total_nums = data_info['total_nums']
file_nums = data_info['file_nums']
dir_nums = data_info['dir_nums']
total_pages = ceil(total_nums / limit)
if page > total_pages:
page = total_pages
page = 1 if page<1 else page
# 根据分页参数对结果进行切片
start_idx = (page - 1) * limit
end_idx = start_idx + limit
paginated_data = data_dirs[start_idx:end_idx]
if not is_windows:
# directory, filename = os.path.split(path)
if path == "/":
directory_dict_list = []
else:
directory_list = path.split(os.path.sep)
directory_dict_list = [{'name': directory_list[i-1], 'url': os.sep.join(directory_list[:i])} for i in range(1,len(directory_list)+1)]
else:
# 去掉盘符部分
drive_name = path.split(':')[0]
if drive_name:
drive_name.lower()
try:
path_without_drive =path.split(':')[1] if drive_name else None
except:
return ErrorResponse(msg="路径错误:%s"%path)
# 按斜杠分割路径
if not path_without_drive or path_without_drive == "/":
directory_list = []
directory_dict_list = []
else:
directory_list = path_without_drive.strip('/').strip('\\').split('/')
# 获取每个名称的路径
path_list = ['/'.join(directory_list[:i+1]) for i in range(len(directory_list))]
directory_dict_list = [{'name': name, 'url': drive_name+":/"+path_list[i]} for i, name in enumerate(directory_list)]
if drive_name:
directory_dict_list.insert(0, {'name':drive_name+"",'url':drive_name+":/"})
else:
directory_dict_list.insert(0, {'name':drive_name,'url':""})
data = {
'data':paginated_data,
'path':path,
'paths':directory_dict_list,
'file_nums':file_nums,
'dir_nums':dir_nums,
'is_windows':is_windows
}
return SuccessResponse(data=data,total=total_nums,page=page,limit=limit)
elif action == "get_filedir_attribute":
data = get_filedir_attribute(path,is_windows=is_windows)
if not data:
return ErrorResponse(msg="目标不存在/或不支持查看")
return DetailResponse(data=data)
elif action == "calc_size":
size = get_directory_size(path)
data = {"size":size}
return DetailResponse(data=data)
elif action == "batch_operate":
reqData['path'] = path
c_type = reqData.get('type',None)
c_type_name = get_type_name(c_type)
isok,msg,code,data = batch_operate(param=reqData,is_windows=is_windows)
if not isok:
return ErrorResponse(code=code,msg=msg,data=data)
return DetailResponse(msg=msg,data=data)
elif action == "create_file":
filename = reqData.get("filename","")
if not filename:
return ErrorResponse(msg="请填写文件名")
path = path+"/"+filename
create_file(path=path,is_windows=is_windows)
return DetailResponse(msg="创建成功")
elif action == "create_dir":
dirname = reqData.get("dirname","")
if not dirname:
return ErrorResponse(msg="请填写目录名")
path = path+"/"+dirname
create_dir(path=path,is_windows=is_windows)
return DetailResponse(msg="创建成功")
elif action == "delete_file":
delete_file(path=path,is_windows=is_windows)
return DetailResponse(msg="删除成功")
elif action == "delete_dir":
delete_dir(path=path,is_windows=is_windows)
return DetailResponse(msg="删除成功")
elif action == "rename_file":
sname = reqData.get("sname","")
dname = reqData.get("dname","")
if not sname or not dname:
return ErrorResponse(msg="参数错误")
sPath = path+"/"+sname
dPath = path+"/"+dname
rename_file(sPath=sPath,dPath=dPath,is_windows=is_windows)
return DetailResponse(msg="重命名成功")
elif action == "copy_file":
sPath = reqData.get("spath","")
name = reqData.get("name","")
cover = reqData.get("cover",False)
if not sPath or not name:
return ErrorResponse(msg="参数错误")
dPath = path+"/"+name
if not cover:
if os.path.exists(dPath):
return ErrorResponse(code=4050,msg="目标存在相同文件")
copy_file(sPath=sPath,dPath=dPath,is_windows=is_windows)
return DetailResponse(msg="复制成功")
elif action == "copy_dir":
sPath = reqData.get("spath","")
name = reqData.get("name","")
cover = reqData.get("cover",False)
if not sPath or not name:
return ErrorResponse(msg="参数错误")
dPath = path+"/"+name
if not cover:
if os.path.exists(dPath):
return ErrorResponse(code=4050,msg="目标存在相同目录")
copy_dir(sPath=sPath,dPath=dPath,is_windows=is_windows,cover=cover)
return DetailResponse(msg="复制成功")
elif action == "move_file":
spath = reqData.get("spath","")
name = reqData.get("name","")
cover = reqData.get("cover",False)
if not spath or not name:
return ErrorResponse(msg="参数错误")
dPath = path+"/"+name
if not cover:
if os.path.exists(dPath):
return ErrorResponse(code=4050,msg="目标存在相同目录/文件")
move_file(sPath=spath,dPath=dPath,is_windows=is_windows,cover=cover)
return DetailResponse(msg="移动成功")
elif action == "read_file_body":
ext = get_filename_ext(path)
if ext in ['msi','psd','dll','sys','gz', 'zip', 'rar','7z', 'bz2', 'exe', 'db','sqlite','sqlite3','.mdb', 'pdf', 'doc', 'xls', 'docx', 'xlsx', 'ppt','pptx','mp4','flv','avi', 'png', 'gif', 'jpg', 'jpeg', 'bmp', 'icon', 'ico', 'pyc','class', 'so', 'pyd']:
return ErrorResponse(msg="该文件不支持在线编辑")
if not os.path.exists(path):
return ErrorResponse(msg="该文件不存在")
size = os.path.getsize(path)
if size>3145728:#大于3M不建议在线编辑
return ErrorResponse(msg="文件过大,不支持在线编辑")
content = ""
encoding = "utf-8"
try:
with open(path, 'r', encoding="utf-8", errors='ignore') as file:
content = file.read()
except PermissionError as e:
return ErrorResponse(msg="文件被占用,暂无法打开")
except OSError as e:
return ErrorResponse(msg="操作系统错误,暂无法打开")
except:
try:
with open(path, 'r', encoding="GBK", errors='ignore') as file:
content = file.read()
encoding = "GBK"
except PermissionError as e:
return ErrorResponse(msg="文件被占用,暂无法打开")
except OSError as e:
return ErrorResponse(msg="操作系统错误,暂无法打开")
except Exception as e:
return ErrorResponse(msg="文件编码不兼容")
data = {
'st_mtime':str(int(os.stat(path).st_mtime)),
'content':content,
'size':size,
'encoding':encoding,
'language':auto_detect_file_language(path)
}
return DetailResponse(data=data,msg="获取成功")
elif action == "save_file_body":
content = reqData.get("content",None)
st_mtime = reqData.get("st_mtime",None)
force = reqData.get("force",False)
if not force and st_mtime and not st_mtime == str(int(os.stat(path).st_mtime)):
return ErrorResponse(code=4050,msg="在线文件可能发生变动,是否继续保存")
WriteFile(path,content)
return DetailResponse({'st_mtime':str(int(os.stat(path).st_mtime))},msg="保存成功")
elif action == "set_file_access":
user = reqData.get("user",None)
group = reqData.get("group",None)
access = reqData.get("access",False)
issub = reqData.get("issub",True)
if not user or not group:return ErrorResponse(msg="所属组/者不能为空")
if not os.path.exists(path):
return ErrorResponse(msg="路径不存在")
numeric_pattern = re.compile(r'^[0-7]{3}$')
if not numeric_pattern.match(str(access)):return ErrorResponse(msg="权限格式错误")
if is_windows:
return ErrorResponse(msg="windows目前还不支持此功能")
if check_in_black_list(path=path,is_windows=is_windows):
return ErrorResponse(msg="该目录不能设置权限")
issub_str ="-R" if issub else ""
command = f"chmod {issub_str} {access} {path}"
res,err = RunCommand(command)
if err:
return ErrorResponse(msg=err)
command1 = f"chown {issub_str} {user}:{group} {path}"
res1,err1 =RunCommand(command1)
if err1:
return ErrorResponse(msg=err1)
return DetailResponse({'st_mtime':str(int(os.stat(path).st_mtime))},msg="设置成功")
data = {}
return DetailResponse(data=data)
class RYGetFileDownloadView(APIView):
"""
get:
根据文件token进行文件下载
"""
permission_classes = []
authentication_classes = []
def get(self, request):
reqData = get_parameter_dic(request)
filename = reqData.get("filename",None)
token = reqData.get("token",None)
expires = reqData.get("expires",0)
isok,msg = validate_file_token(filename=filename,expires=expires,token=token)
if not isok:
return ResponseNginx404()
if not filename:
return ErrorResponse(msg="参数错误")
if not os.path.exists(filename):
return ErrorResponse(msg="文件不存在")
if not os.path.isfile(filename):
return ErrorResponse(msg="参数错误")
file_size = os.path.getsize(filename)
content_type, encoding = mimetypes.guess_type(filename)
content_type = content_type or 'application/octet-stream'
response = StreamingHttpResponse(open(filename, 'rb'), content_type=content_type)
response['Content-Disposition'] = f'attachment;filename="{escape_uri_path(os.path.basename(filename))}"'
response['Content-Length'] = file_size
return response
class RYFileDownloadView(APIView):
"""
post:
文件下载
"""
permission_classes = [IsAuthenticated,CustomPermission]
authentication_classes = [JWTAuthentication]
def post(self, request):
reqData = get_parameter_dic(request)
filename = reqData.get("filename",None)
if not filename:
return ErrorResponse(msg="参数错误")
if not os.path.exists(filename):
return ErrorResponse(msg="文件不存在")
if not os.path.isfile(filename):
return ErrorResponse(msg="参数错误")
file_size = os.path.getsize(filename)
response = FileResponse(open(filename, 'rb'))
response['content_type'] = "application/octet-stream"
response['Content-Disposition'] = f'attachment;filename="{escape_uri_path(os.path.basename(filename))}"'
# response['Content-Disposition'] = f'attachment; filename="{filename}"'
response['Content-Length'] = file_size # 设置文件大小
return response
def generate_file_token(filename,expire=None):
secret_key = settings.SECRET_KEY
if not expire:
expire = 43200
expire_time = int(time.time()) + expire # 设置过期时间
# 生成安全签名 token
signature = md5(f"{filename}-{expire_time}-{secret_key}")
return {
'token':signature,
'expires':expire_time,
'filename':filename
}
def validate_file_token(filename,expires,token):
expires = int(expires)
current_time = int(time.time())
secret_key = settings.SECRET_KEY
# 校验安全签名 token
expected_signature = md5(f"{filename}-{expires}-{secret_key}")
if token == expected_signature:
if current_time <= expires:
return True,"ok"
else:
return False,"token已过期"
else:
return False,"无效的token"
class RYFileTokenView(APIView):
"""
post:
获取文件访问token
"""
permission_classes = [IsAuthenticated,CustomPermission]
authentication_classes = [JWTAuthentication]
def post(self, request):
reqData = get_parameter_dic(request)
filename = reqData.get("filename",None)
if not filename:
return ErrorResponse(msg="参数错误")
if not os.path.exists(filename):
return ErrorResponse(msg="文件不存在")
data = generate_file_token(filename=filename)
return DetailResponse(data=data)
class RYFileMediaView(APIView):
"""
get:
媒体文件
"""
permission_classes = []
authentication_classes = []
def get(self, request):
reqData = get_parameter_dic(request)
filename = reqData.get("filename",None)
token = reqData.get("token",None)
expires = reqData.get("expires",0)
isok,msg = validate_file_token(filename=filename,expires=expires,token=token)
if not isok:
return ResponseNginx404()
if not filename:
return ErrorResponse(msg="参数错误")
if not os.path.exists(filename):
return ErrorResponse(msg="文件不存在")
if not os.path.isfile(filename):
return ErrorResponse(msg="参数错误")
content_type, encoding = mimetypes.guess_type(filename)
content_type = content_type or 'application/octet-stream'
if content_type in ['video/mp4','video/ogg', 'video/flv', 'video/avi', 'video/wmv', 'video/rmvb','audio/mp3','audio/x-m4a','audio/mpeg','audio/ogg']:
response = stream_video(request, filename)#支持视频流媒体播放
return response
return ErrorResponse(msg="限制只能媒体文件")
class RYFileUploadView(APIView):
"""
post:
文件上传(支持分片断点续传)
"""
permission_classes = [IsAuthenticated,CustomPermission]
authentication_classes = [JWTAuthentication]
throttle_classes=[]
def post(self, request):
reqData = get_parameter_dic(request)
file = request.FILES.get('lyfile',None)
filechunk_name = reqData.get('lyfilechunk',None)
path = reqData.get("path",None)
if not path:
return ErrorResponse(msg="参数错误")
if path[-1] == '/':
path = path[:-1]
if file:
save_path = path+"/"+file.name
if path and not os.path.exists(path):
os.makedirs(path)
with open(save_path, 'wb+') as destination:
for ck in file.chunks():
destination.write(ck)
return DetailResponse(data=None,msg="上传成功")
elif filechunk_name:
chunk = reqData.get('chunk')
chunkIndex = int(reqData.get('chunkIndex',0))
chunkCount = int(reqData.get('chunkCount',0))
if not chunkCount:
return ErrorResponse(msg="参数错误")
if path and not os.path.exists(path):
os.makedirs(path)
# 保存文件分片
with open(f'{path}/{filechunk_name}.part{chunkIndex}', 'wb') as destination:
for content in chunk.chunks():
destination.write(content)
# 如果所有分片上传完毕,合并文件
if chunkIndex == chunkCount - 1:
with open(f'{path}/{filechunk_name}', 'ab') as final_destination:
for i in range(chunkCount):
with open(f'{path}/{filechunk_name}.part{i}', 'rb') as part_file:
final_destination.write(part_file.read())
os.remove(f'{path}/{filechunk_name}.part{i}') # 删除临时分片文件
return DetailResponse(data=None,msg="上传成功")
return DetailResponse(data=None,msg=f'分片{chunkIndex}上传成功')
else:
return ErrorResponse(msg="参数错误")