181 lines
4.8 KiB
TypeScript
181 lines
4.8 KiB
TypeScript
![]() |
import { useState } from 'react';
|
|||
|
import { useTusUpload } from './useTusUpload';
|
|||
|
|
|||
|
interface DownloadProgress {
|
|||
|
loaded: number;
|
|||
|
total: number;
|
|||
|
percentage: number;
|
|||
|
}
|
|||
|
|
|||
|
export function useFileDownload() {
|
|||
|
const { getFileUrlByFileId, serverUrl } = useTusUpload();
|
|||
|
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress | null>(null);
|
|||
|
const [isDownloading, setIsDownloading] = useState(false);
|
|||
|
const [downloadError, setDownloadError] = useState<string | null>(null);
|
|||
|
|
|||
|
// 直接下载文件(浏览器处理)
|
|||
|
const downloadFile = (fileId: string, filename?: string) => {
|
|||
|
const url = getFileUrlByFileId(fileId);
|
|||
|
const link = document.createElement('a');
|
|||
|
link.href = url;
|
|||
|
if (filename) {
|
|||
|
link.download = filename;
|
|||
|
}
|
|||
|
link.target = '_blank';
|
|||
|
document.body.appendChild(link);
|
|||
|
link.click();
|
|||
|
document.body.removeChild(link);
|
|||
|
};
|
|||
|
|
|||
|
// 带进度的文件下载
|
|||
|
const downloadFileWithProgress = async (
|
|||
|
fileId: string,
|
|||
|
filename?: string,
|
|||
|
onProgress?: (progress: DownloadProgress) => void,
|
|||
|
): Promise<Blob> => {
|
|||
|
return new Promise(async (resolve, reject) => {
|
|||
|
setIsDownloading(true);
|
|||
|
setDownloadError(null);
|
|||
|
setDownloadProgress(null);
|
|||
|
|
|||
|
try {
|
|||
|
const url = getFileUrlByFileId(fileId);
|
|||
|
const response = await fetch(url);
|
|||
|
|
|||
|
if (!response.ok) {
|
|||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|||
|
}
|
|||
|
|
|||
|
const contentLength = response.headers.get('Content-Length');
|
|||
|
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|||
|
let loaded = 0;
|
|||
|
|
|||
|
const reader = response.body?.getReader();
|
|||
|
if (!reader) {
|
|||
|
throw new Error('Failed to get response reader');
|
|||
|
}
|
|||
|
|
|||
|
const chunks: Uint8Array[] = [];
|
|||
|
|
|||
|
while (true) {
|
|||
|
const { done, value } = await reader.read();
|
|||
|
|
|||
|
if (done) break;
|
|||
|
|
|||
|
if (value) {
|
|||
|
chunks.push(value);
|
|||
|
loaded += value.length;
|
|||
|
|
|||
|
const progress = {
|
|||
|
loaded,
|
|||
|
total,
|
|||
|
percentage: total > 0 ? Math.round((loaded / total) * 100) : 0,
|
|||
|
};
|
|||
|
|
|||
|
setDownloadProgress(progress);
|
|||
|
onProgress?.(progress);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 创建 Blob
|
|||
|
const blob = new Blob(chunks);
|
|||
|
|
|||
|
// 如果提供了文件名,自动下载
|
|||
|
if (filename) {
|
|||
|
const downloadUrl = URL.createObjectURL(blob);
|
|||
|
const link = document.createElement('a');
|
|||
|
link.href = downloadUrl;
|
|||
|
link.download = filename;
|
|||
|
document.body.appendChild(link);
|
|||
|
link.click();
|
|||
|
document.body.removeChild(link);
|
|||
|
URL.revokeObjectURL(downloadUrl);
|
|||
|
}
|
|||
|
|
|||
|
setIsDownloading(false);
|
|||
|
setDownloadProgress(null);
|
|||
|
resolve(blob);
|
|||
|
} catch (error) {
|
|||
|
const errorMessage = error instanceof Error ? error.message : 'Download failed';
|
|||
|
setDownloadError(errorMessage);
|
|||
|
setIsDownloading(false);
|
|||
|
setDownloadProgress(null);
|
|||
|
reject(new Error(errorMessage));
|
|||
|
}
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
// 预览文件(在新窗口打开)
|
|||
|
const previewFile = (fileId: string) => {
|
|||
|
const url = getFileUrlByFileId(fileId);
|
|||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|||
|
};
|
|||
|
|
|||
|
// 获取文件的 Blob URL(用于预览)
|
|||
|
const getFileBlobUrl = async (fileId: string): Promise<string> => {
|
|||
|
try {
|
|||
|
const blob = await downloadFileWithProgress(fileId);
|
|||
|
return URL.createObjectURL(blob);
|
|||
|
} catch (error) {
|
|||
|
throw new Error('Failed to create blob URL');
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// 复制文件链接到剪贴板
|
|||
|
const copyFileLink = async (fileId: string): Promise<void> => {
|
|||
|
try {
|
|||
|
const url = getFileUrlByFileId(fileId);
|
|||
|
await navigator.clipboard.writeText(url);
|
|||
|
} catch (error) {
|
|||
|
throw new Error('Failed to copy link');
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// 检查文件是否可以预览(基于 MIME 类型)
|
|||
|
const canPreview = (mimeType: string): boolean => {
|
|||
|
const previewableTypes = [
|
|||
|
'image/', // 所有图片
|
|||
|
'application/pdf',
|
|||
|
'text/',
|
|||
|
'video/',
|
|||
|
'audio/',
|
|||
|
];
|
|||
|
|
|||
|
return previewableTypes.some((type) => mimeType.startsWith(type));
|
|||
|
};
|
|||
|
|
|||
|
// 获取文件类型图标
|
|||
|
const getFileIcon = (mimeType: string): string => {
|
|||
|
if (mimeType.startsWith('image/')) return '🖼️';
|
|||
|
if (mimeType.startsWith('video/')) return '🎥';
|
|||
|
if (mimeType.startsWith('audio/')) return '🎵';
|
|||
|
if (mimeType === 'application/pdf') return '📄';
|
|||
|
if (mimeType.startsWith('text/')) return '📝';
|
|||
|
if (mimeType.includes('word')) return '📝';
|
|||
|
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊';
|
|||
|
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📊';
|
|||
|
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('archive')) return '📦';
|
|||
|
return '📁';
|
|||
|
};
|
|||
|
|
|||
|
return {
|
|||
|
// 状态
|
|||
|
downloadProgress,
|
|||
|
isDownloading,
|
|||
|
downloadError,
|
|||
|
|
|||
|
// 方法
|
|||
|
downloadFile,
|
|||
|
downloadFileWithProgress,
|
|||
|
previewFile,
|
|||
|
getFileBlobUrl,
|
|||
|
copyFileLink,
|
|||
|
|
|||
|
// 工具函数
|
|||
|
canPreview,
|
|||
|
getFileIcon,
|
|||
|
getFileUrlByFileId,
|
|||
|
serverUrl,
|
|||
|
};
|
|||
|
}
|