import { transformData } from '@/components/utils/objectFlat'; import * as MediaLibrary from 'expo-media-library'; import { convertHeicToJpeg } from '../image-process/heicConvert'; import { compressImage } from '../image-process/imageCompress'; import { uploadVideoThumbnail } from '../video-process/videoThumbnail'; import { addMaterial, confirmUpload, getUploadUrl } from './api'; import { ExtendedAsset } from './types'; import { checkMediaLibraryPermission, getFileExtension, getMimeType } from './utils'; import { getUploadTaskStatus, updateUploadTaskStatus, updateUploadTaskProgress } from '../db'; // 基础文件上传实现 export const uploadFile = async (file: File, uploadUrl: string): Promise => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error(`Upload failed with status ${xhr.status}`)); } }; xhr.onerror = () => { reject(new Error('Network error during upload')); }; xhr.send(file); }); }; // 处理单个媒体文件上传的核心逻辑 export const processAndUploadMedia = async ( asset: ExtendedAsset, onProgress?: (progress: { loaded: number; total: number }) => void ) => { try { // 1. 文件去重检查 (从数据库获取状态) const existingTask = await getUploadTaskStatus(asset.uri); if (existingTask && (existingTask.status === 'success' || existingTask.status === 'skipped')) { console.log(`File ${asset.uri} already ${existingTask.status}, skipping processing.`); return null; // 返回 null 表示已上传或已跳过,调用方可以据此过滤 } // 标记为正在上传 await updateUploadTaskStatus(asset.uri, 'uploading'); // 2. 检查权限 const { hasPermission } = await checkMediaLibraryPermission(); if (!hasPermission) { throw new Error('No media library permission'); } const isVideo = asset.mediaType === 'video'; // 3. 上传原始文件 const uploadOriginalFile = async () => { let fileToUpload: File; const mimeType = getMimeType(asset.filename || '', isVideo); let filename = asset.filename || `${isVideo ? 'video' : 'image'}_${Date.now()}_original.${isVideo ? (getFileExtension(asset.filename || 'mp4') || 'mp4') : 'jpg'}`; // 处理 HEIC 格式 if (filename.toLowerCase().endsWith('.heic') || filename.toLowerCase().endsWith('.heif')) { fileToUpload = await convertHeicToJpeg(asset.uri); filename = filename.replace(/\.(heic|heif)$/i, '.jpg'); } else { const assetInfo = await MediaLibrary.getAssetInfoAsync(asset.id, { shouldDownloadFromNetwork: true }); if (!assetInfo.localUri) throw new Error('无法获取资源的本地路径'); const fileExtension = getFileExtension(assetInfo.filename || '') || (isVideo ? 'mp4' : 'jpg'); if (!filename.toLowerCase().endsWith(`.${fileExtension}`)) { const baseName = filename.split('.')[0]; filename = `${baseName}.${fileExtension}`; } const response = await fetch(assetInfo.localUri); const blob = await response.blob(); fileToUpload = new File([blob], filename, { type: mimeType }); } let exifData = {}; if (asset.exif) { try { exifData = transformData({ ...asset, exif: { ...asset.exif, '{MakerApple}': undefined } }); } catch (exifError) { console.warn('处理 EXIF 数据时出错:', exifError); } } const { upload_url, file_id } = await getUploadUrl(fileToUpload, { originalUri: asset.uri, creationTime: asset.creationTime, mediaType: isVideo ? 'video' : 'image', isCompressed: false, ...exifData, GPSVersionID: undefined }); await uploadFileWithProgress(fileToUpload, upload_url, async (progress) => { if (onProgress) onProgress(progress); const percentage = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0; await updateUploadTaskProgress(asset.uri, Math.round(percentage * 0.5)); // 原始文件占总进度的50% }); await confirmUpload(file_id); return { success: true, file_id, filename: fileToUpload.name }; }; // 4. 上传压缩文件(仅图片) const uploadCompressedFile = async () => { if (isVideo) return { success: true, file_id: null }; try { const manipResult = await compressImage(asset.uri); const response = await fetch(manipResult.uri); const blob = await response.blob(); const filename = asset.filename ? `compressed_${asset.filename}` : `image_${Date.now()}_compressed.jpg`; const compressedFile = new File([blob], filename, { type: 'image/jpeg' }); const { upload_url, file_id } = await getUploadUrl(compressedFile, { originalUri: asset.uri, creationTime: asset.creationTime, mediaType: 'image', isCompressed: true }); await uploadFileWithProgress(compressedFile, upload_url, async (progress) => { // For compressed files, we can't easily report byte progress relative to the whole process, // as we don't know the compressed size in advance. We'll just update the DB progress. const percentage = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0; await updateUploadTaskProgress(asset.uri, 50 + Math.round(percentage * 0.5)); // 压缩文件占总进度的后50% }); await confirmUpload(file_id); return { success: true, file_id }; } catch (error) { console.error('Error uploading compressed file:', error); return { success: false, error, file_id: null }; } }; // 执行上传 const originalResult = await uploadOriginalFile(); if (!originalResult.success) { throw new Error('Original file upload failed'); } let compressedResult: { success: boolean; file_id?: string | null; error?: any } = { success: true, file_id: null }; if (!isVideo) { compressedResult = await uploadCompressedFile(); if (originalResult.file_id && compressedResult.file_id) { await addMaterial(originalResult.file_id, compressedResult.file_id); } } else { const thumbnailResult = await uploadVideoThumbnail(asset); if (thumbnailResult.success && originalResult.file_id && thumbnailResult.file_id) { await addMaterial(originalResult.file_id, thumbnailResult.file_id); } } // 标记为已上传 await updateUploadTaskStatus(asset.uri, 'success', originalResult.file_id); await updateUploadTaskProgress(asset.uri, 100); return { id: asset.id, originalSuccess: originalResult.success, compressedSuccess: compressedResult.success, fileIds: { original: originalResult.file_id, compressed: compressedResult.file_id } }; } catch (error: any) { console.error('Error processing media upload for asset:', asset.uri, error); // 标记为失败 await updateUploadTaskStatus(asset.uri, 'failed'); await updateUploadTaskProgress(asset.uri, 0); return { id: asset.id, originalSuccess: false, compressedSuccess: false, error: error.message }; } }; export const uploadFileWithProgress = async ( file: File, uploadUrl: string, onProgress?: (progress: { loaded: number; total: number }) => void, timeout: number = 30000 ): Promise => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); let timeoutId: number | undefined; xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); // 进度监听 if (onProgress) { xhr.upload.onprogress = (event) => { if (event.lengthComputable) { onProgress({ loaded: event.loaded, total: event.total }); } }; } xhr.onload = () => { clearTimeout(timeoutId); if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error(`Upload failed with status ${xhr.status}`)); } }; xhr.onerror = () => { clearTimeout(timeoutId); reject(new Error('Network error during upload')); }; // 超时处理 timeoutId = setTimeout(() => { xhr.abort(); reject(new Error('上传超时,请检查网络连接')); }, timeout); xhr.send(file); }); };