This commit is contained in:
Rao 2025-04-03 13:27:58 +08:00
parent 6985ee863f
commit 0dad0e18ec
7 changed files with 168 additions and 223 deletions

View File

@ -27,7 +27,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/schedule": "^4.1.0",
"@nestjs/schedule": "^4.1.2",
"@nestjs/websockets": "^10.3.10",
"@nice/common": "workspace:*",
"@nice/tus": "workspace:*",

View File

@ -3,6 +3,9 @@ import { customAlphabet } from 'nanoid-cjs';
import { db } from '@nice/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { ResourceService } from '@server/models/resource/resource.service';
import * as fs from 'fs'
import * as path from 'path'
export interface ShareCode {
id: string;
@ -44,7 +47,7 @@ export class ShareCodeService {
const resource = await this.resourceService.findUnique({
where: { fileId },
});
console.log('完整 fileId:', fileId); // 确保与前端一致
this.logger.log('完整 fileId:', fileId); // 确保与前端一致
if (!resource) {
throw new NotFoundException('文件不存在');
@ -53,7 +56,7 @@ export class ShareCodeService {
// 生成分享码
const code = this.generateCode();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
//const expiresAt = new Date(Date.now() + 10 * 1000); // 24小时后过期
// 查找是否已有分享码记录
const existingShareCode = await db.shareCode.findUnique({
where: { fileId },
@ -96,7 +99,7 @@ export class ShareCodeService {
async validateAndUseCode(code: string): Promise<ShareCode | null> {
try {
console.log(`尝试验证分享码: ${code}`);
this.logger.log(`尝试验证分享码: ${code}`);
// 查找有效的分享码
const shareCode = await db.shareCode.findFirst({
@ -107,10 +110,10 @@ export class ShareCodeService {
},
});
console.log('查询结果:', shareCode);
this.logger.log('查询结果:', shareCode);
if (!shareCode) {
console.log('分享码无效或已过期');
this.logger.log('分享码无效或已过期');
return null;
}
@ -132,21 +135,74 @@ export class ShareCodeService {
}
// 每天清理过期的分享码
//@Cron('*/30 * * * * *')
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanupExpiredShareCodes() {
try {
const result = await db.shareCode.deleteMany({
const shareCodes = await db.shareCode.findMany({
where: {
OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }],
}
})
this.logger.log('需要清理的分享码:', shareCodes);
shareCodes.forEach(code => {
this.cleanupUploadFolder(code.fileId);
})
const result = await db.shareCode.deleteMany({
where: {
fileId: {
in: shareCodes.map(code => code.fileId)
}
},
});
this.logger.log(`Cleaned up ${result.count} expired share codes`);
const deleteResource = await this.resourceService.deleteMany({
where: {
fileId: {
in: shareCodes.map(code => code.fileId)
}
}
})
this.logger.log(`Cleaned up ${result.count} ${deleteResource.count} expired share codes`);
} catch (error) {
this.logger.error('Failed to cleanup expired share codes', error);
}
}
async cleanupUploadFolder(file?: string) {
//const uploadDir = path.join(__dirname, '../../../uploads');
const uploadDir = path.join('/data/uploads/', file || '');
this.logger.log('uploadDir:', uploadDir);
try {
if (!fs.existsSync(uploadDir)) {
this.logger.warn(`Upload directory does not exist: ${uploadDir}`);
return;
}
// 递归删除文件夹及其内容
this.deleteFolderRecursive(uploadDir);
this.logger.log(`Cleaned up upload folder: ${uploadDir}`);
} catch (error) {
this.logger.error('读取上传目录失败:', error);
return;
}
}
private deleteFolderRecursive(dirPath: string) {
if (fs.existsSync(dirPath)) {
fs.readdirSync(dirPath).forEach((file) => {
const filePath = path.join(dirPath, file);
if (fs.statSync(filePath).isDirectory()) {
// 递归删除子目录
this.deleteFolderRecursive(filePath);
} else {
// 删除文件
fs.unlinkSync(filePath);
this.logger.log(`Deleted file: ${filePath}`);
}
});
// 删除空文件夹
fs.rmdirSync(dirPath);
this.logger.log(`Deleted folder: ${dirPath}`);
}
}
// 获取分享码信息
async getShareCodeInfo(code: string): Promise<ShareCode | null> {
try {
@ -192,10 +248,10 @@ export class ShareCodeService {
// 根据分享码获取文件
async getFileByShareCode(code: string) {
console.log('收到验证分享码请求code:', code);
this.logger.log('收到验证分享码请求code:', code);
const shareCode = await this.validateAndUseCode(code);
console.log('验证分享码结果:', shareCode);
this.logger.log('验证分享码结果:', shareCode);
if (!shareCode) {
throw new NotFoundException('分享码无效或已过期');
@ -205,8 +261,9 @@ export class ShareCodeService {
const resource = await this.resourceService.findUnique({
where: { fileId: shareCode.fileId },
});
console.log('获取到的资源信息:', resource);
this.logger.log('获取到的资源信息:', resource);
const { filename } = resource.meta as any as ResourceMeta
const fileUrl = resource?.url
if (!resource) {
throw new NotFoundException('文件不存在');
}
@ -216,22 +273,23 @@ export class ShareCodeService {
fileId: shareCode.fileId,
fileName: filename || 'downloaded_file',
code: shareCode.code,
expiresAt: shareCode.expiresAt
expiresAt: shareCode.expiresAt,
url: fileUrl,
};
console.log('返回给前端的数据:', response); // 添加日志
this.logger.log('返回给前端的数据:', response); // 添加日志
return response;
}
// 根据文件ID生成分享码
async generateShareCodeByFileId(fileId: string) {
try {
console.log('收到生成分享码请求fileId:', fileId);
this.logger.log('收到生成分享码请求fileId:', fileId);
const result = await this.generateShareCode(fileId);
console.log('生成分享码结果:', result);
this.logger.log('生成分享码结果:', result);
return result;
} catch (error) {
console.error('生成分享码错误:', error);
this.logger.error('生成分享码错误:', error);
return error
}

View File

@ -1,233 +1,95 @@
import { useTusUpload } from "@web/src/hooks/useTusUpload";
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
import { useState, useRef, useCallback, useEffect } from "react";
import { message, Progress, Button, Tabs, DatePicker, Form } from "antd";
import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons";
import { message, Tabs, Form, Spin } from "antd";
import { env } from '../../../env'
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
import { useState } from "react";
const { TabPane } = Tabs;
export default function DeptSettingPage() {
const [uploadedFileId, setUploadedFileId] = useState<string>('');
const [uploadedFileName, setUploadedFileName] = useState<string>('');
const [fileNameMap, setFileNameMap] = useState<Record<string, string>>({});
const [uploadedFiles, setUploadedFiles] = useState<{ id: string, name: string }[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [expireTime, setExpireTime] = useState<Date | null>(null);
const dropRef = useRef<HTMLDivElement>(null);
const [form] = Form.useForm();
const [currentFile, setCurrentFile] = useState<string[]>([])
const uploadFileId = Form.useWatch(["file"], form)?.[0]
// 使用您的 useTusUpload hook
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload();
// 清除已上传文件
const handleClearFile = () => {
setUploadedFileId('');
setUploadedFileName('');
setUploadedFiles([]);
setFileNameMap({});
};
// 处理文件上传
const handleFileSelect = async (file: File) => {
// 限制:如果已有上传文件,则提示用户
if (uploadedFiles.length > 0) {
message.warning('只能上传一个文件,请先删除已上传的文件');
return;
}
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
handleFileUpload(
file,
async (result) => {
setUploadedFileId(result.fileId);
setUploadedFileName(result.fileName);
// 添加到已上传文件列表
setUploadedFiles([{ id: result.fileId, name: file.name }]);
// 在前端保存文件名映射(用于当前会话)
setFileNameMap({
[result.fileId]: file.name
});
// 上传成功后保存原始文件名到数据库
try {
console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId);
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileId: result.fileId,
fileName: file.name
}),
});
const responseText = await response.text();
console.log('保存文件名响应:', response.status, responseText);
if (!response.ok) {
console.error('保存文件名失败:', responseText);
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
} else {
console.log('文件名保存成功:', file.name);
}
} catch (error) {
console.error('保存文件名请求失败:', error);
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
}
message.success('文件上传成功');
},
(error) => {
message.error('上传失败:' + error.message);
},
fileKey
);
};
// 处理多个文件上传 - 已移除
// const handleFilesUpload = (file: File) => {
// handleFileSelect(file);
// };
// 处理文件删除
const handleDeleteFile = async (fileId: string) => {
try {
// 可以添加删除文件的API调用
// const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/delete/${fileId}`, {
// method: 'DELETE'
// });
// if (!response.ok) {
// throw new Error('删除文件失败');
// }
// 无论服务器删除是否成功,前端都需要更新状态
setUploadedFiles([]);
setUploadedFileId('');
setUploadedFileName('');
setFileNameMap({});
message.success('文件已删除');
} catch (error) {
console.error('删除文件错误:', error);
message.error('删除文件失败');
}
};
// 拖拽相关处理函数
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
}, []);
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
}, []);
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
handleFileSelect(e.dataTransfer.files[0]);
}, []);
const [isGetingFileId, setIsGetingFileId] = useState(false);
// 处理分享码生成成功
const handleShareSuccess = (code: string) => {
message.success('分享码生成成功:' + code);
// 可以在这里添加其他逻辑,比如保存到历史记录
};
}
// 处理分享码验证成功
const handleValidSuccess = async (fileId: string, fileName: string) => {
const handleValidSuccess = async (fileUrl: string, fileName: string) => {
setIsGetingFileId(true);
try {
// 构建下载URL包含文件名参数
const downloadUrl = `/upload/download/${fileId}?fileName=${encodeURIComponent(fileName)}`;
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error('文件下载失败');
}
// 创建下载链接
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
console.log('文件url:', fileUrl);
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
console.log('下载URL:', downloadUrl);
const link = document.createElement('a');
link.href = url;
link.href = downloadUrl;
// 直接使用传入的 fileName
link.download = fileName;
link.target = '_blank'; // 在新标签页中打开
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success('文件下载开始');
} catch (error) {
console.error('下载失败:', error);
message.error('文件下载失败');
} finally {
setIsGetingFileId(false);
}
};
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<span className="text-2xl py-4"></span>
<Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload">
{/* 文件上传区域 */}
<div style={{ marginBottom: '40px' }}>
<span className="text-lg block text-zinc-700 py-2"></span>
{/* 如果没有已上传文件,显示上传区域 */}
<Form form={form}>
<Form.Item name="file">
<TusUploader
multiple={false}
style={"w-full py-4"}
></TusUploader>
</Form.Item>
</Form>
</div>
<>
{
isGetingFileId ?
(<Spin spinning={isGetingFileId} fullscreen />) :
(<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<span className="text-2xl py-4"></span>
<Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload">
{/* 文件上传区域 */}
<div style={{ marginBottom: '40px' }}>
<span className="text-lg block text-zinc-700 py-2"></span>
{/* 如果没有已上传文件,显示上传区域 */}
<Form form={form}>
<Form.Item name="file">
<TusUploader
multiple={false}
style={"w-full py-4"}
></TusUploader>
</Form.Item>
</Form>
</div>
{/* 生成分享码区域 */}
<div style={{ marginBottom: '40px' }}>
<span className="text-lg block text-zinc-700 py-4"></span>
<ShareCodeGenerator
fileId={uploadFileId}
onSuccess={handleShareSuccess}
/>
</div>
</TabPane>
{/* 生成分享码区域 */}
<div style={{ marginBottom: '40px' }}>
<span className="text-lg block text-zinc-700 py-4"></span>
<ShareCodeGenerator
fileId={uploadFileId}
onSuccess={handleShareSuccess}
/>
</div>
</TabPane>
{/* 使用分享码区域 */}
<TabPane tab="下载文件" key="download">
<div>
<span className="text-lg block text-zinc-700 py-4">使</span>
<ShareCodeValidator
onValidSuccess={handleValidSuccess}
/>
</div>
</TabPane>
</Tabs>
</div>)
}
</>
)
{/* 使用分享码区域 */}
<TabPane tab="下载文件" key="download">
<div>
<span className="text-lg block text-zinc-700 py-4">使</span>
<ShareCodeValidator
onValidSuccess={handleValidSuccess}
/>
</div>
</TabPane>
</Tabs>
</div>
);
}

View File

@ -14,7 +14,7 @@ interface ShareCodeGeneratorProps {
interface ShareCodeResponse {
code?: string;
expiresAt?: Date;
}
}
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
fileId,
onSuccess,
@ -39,6 +39,10 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
setCurrentFileId(fileId);
}, [fileId])
const generateCode = async () => {
if (!fileId) {
message.error('请先上传文件');
return;
}
setLoading(true);
console.log('开始生成分享码fileId:', fileId, 'fileName:', fileName);
try {
@ -48,7 +52,7 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
setIsGenerate(true);
setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null);
onSuccess?.(data.code);
message.success('分享码生成成功');
//message.success('分享码生成成功');
} catch (error) {
console.error('生成分享码错误:', error);
message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误'));
@ -56,6 +60,26 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
setLoading(false);
}
};
function copyToClipboard(text) {
if (navigator.clipboard) {
return navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
return Promise.resolve();
}
}
// 组件使用
const handleCopy = (code) => {
copyToClipboard(code)
.then(() => console.log('复制成功'))
.catch(() => console.error('复制失败'));
};
return (
<div style={{ padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
@ -96,7 +120,8 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
<Button
icon={<CopyOutlined />}
onClick={() => {
navigator.clipboard.writeText(shareCode);
handleCopy(shareCode)
//navigator.clipboard.writeText(shareCode);
message.success('分享码已复制');
}}
/>

View File

@ -22,23 +22,23 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
const validateCode = useCallback(() => {
if (!code.trim()) {
message.warning('请输入分享码');
message.warning('请输入正确的分享码');
return;
}
setLoading(true);
try {
console.log('验证分享码返回数据:', result);
onValidSuccess(result.fileId, result.fileName);
message.success(`验证成功,文件名:${result.fileName}`);
onValidSuccess(result.url, result.fileName);
message.success(`验证成功,文件名:${result.fileName},请等待下载...`);
} catch (error) {
console.error('验证分享码失败:', error);
message.error('分享码无效或已过期');
} finally {
setLoading(false);
}
},[result])
},[result,code, onValidSuccess])
return (
<div className={styles.container}>

View File

@ -32,7 +32,7 @@ importers:
specifier: ^10.3.10
version: 10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.15)(rxjs@7.8.1)
'@nestjs/schedule':
specifier: ^4.1.0
specifier: ^4.1.2
version: 4.1.2(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)
'@nestjs/websockets':
specifier: ^10.3.10

BIN
web-dist.zip Normal file

Binary file not shown.