import { fetchApi } from '@/lib/server-api-util'; import * as BackgroundFetch from 'expo-background-fetch'; import * as ImageManipulator from 'expo-image-manipulator'; import * as MediaLibrary from 'expo-media-library'; import * as TaskManager from 'expo-task-manager'; import pLimit from 'p-limit'; import { Alert } from 'react-native'; const BACKGROUND_UPLOAD_TASK = 'background-upload-task'; // 设置最大并发数 const CONCURRENCY_LIMIT = 3; // 同时最多上传3个文件 // 获取指定时间范围内的媒体文件 export const getMediaByDateRange = async (startDate: Date, endDate: Date) => { try { const { status } = await MediaLibrary.requestPermissionsAsync(); if (status !== 'granted') { console.log('Media library permission not granted'); return []; } // 获取媒体资源 const media = await MediaLibrary.getAssetsAsync({ mediaType: ['photo', 'video'], first: 100, // 每次最多获取100个 sortBy: [MediaLibrary.SortBy.creationTime], createdAfter: startDate.getTime(), createdBefore: endDate.getTime(), }); return media.assets; } catch (error) { console.error('Error getting media by date range:', error); return []; } }; // 压缩图片 const compressImage = async (uri: string): Promise<{ uri: string; file: File }> => { try { const manipResult = await ImageManipulator.manipulateAsync( uri, [ { resize: { width: 1200, height: 1200, }, }, ], { compress: 0.7, format: ImageManipulator.SaveFormat.JPEG, base64: false, } ); const response = await fetch(manipResult.uri); const blob = await response.blob(); const filename = uri.split('/').pop() || `image_${Date.now()}.jpg`; const file = new File([blob], filename, { type: 'image/jpeg' }); return { uri: manipResult.uri, file }; } catch (error) { console.error('Error compressing image:', error); throw error; } }; // 获取上传URL const getUploadUrl = async (file: File, metadata: Record = {}): Promise<{ upload_url: string; file_id: string }> => { const body = { filename: file.name, content_type: file.type, file_size: file.size, metadata: { ...metadata, originalName: file.name, fileType: file.type.startsWith('video/') ? 'video' : 'image', isCompressed: 'true', }, }; return await fetchApi<{ upload_url: string; file_id: string }>("/file/generate-upload-url", { method: 'POST', body: JSON.stringify(body) }); }; // 确认上传 const confirmUpload = async (file_id: string) => { return await fetchApi('/file/confirm-upload', { method: 'POST', body: JSON.stringify({ file_id }) }); }; // 新增素材 const addMaterial = async (file: string, compressFile: string) => { await fetchApi('/material', { method: 'POST', body: JSON.stringify([{ "file_id": file, "preview_file_id": compressFile }]) }).catch((error) => { // console.log(error); }) } // 上传文件到URL const uploadFile = async (file: File, uploadUrl: string): Promise => { console.log('=== Starting file upload ==='); console.log('File info:', { name: file.name, type: file.type, size: file.size, lastModified: file.lastModified }); console.log('Upload URL:', uploadUrl); try { const controller = new AbortController(); const timeoutId = setTimeout(() => { console.log('Upload timeout triggered'); controller.abort(); }, 30000); console.log('Sending upload request...'); const startTime = Date.now(); const response = await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type, 'x-oss-forbid-overwrite': 'true' }, body: file, signal: controller.signal }); clearTimeout(timeoutId); const endTime = Date.now(); console.log(`Upload completed in ${endTime - startTime}ms`, { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()) }); if (!response.ok) { const errorText = await response.text().catch(() => ''); console.error('Upload failed with response:', errorText); throw new Error(`Upload failed: ${response.status} ${response.statusText}\n${errorText}`); } console.log('Upload successful'); } catch (error: any) { console.error('Upload error details:', { name: error.name, message: error.message, stack: error.stack }); throw error; } }; // 检查并请求媒体库权限 const checkMediaLibraryPermission = async (): Promise<{ hasPermission: boolean, status?: string }> => { try { const { status, accessPrivileges } = await MediaLibrary.getPermissionsAsync(); // 如果已经授权,直接返回 if (status === 'granted' && accessPrivileges === 'all') { return { hasPermission: true, status }; } // 如果没有授权,请求权限 const { status: newStatus, accessPrivileges: newPrivileges } = await MediaLibrary.requestPermissionsAsync(); const isGranted = newStatus === 'granted' && newPrivileges === 'all'; if (!isGranted) { console.log('Media library permission not granted or limited access'); } return { hasPermission: isGranted, status: newStatus }; } catch (error) { console.error('Error checking media library permission:', error); return { hasPermission: false }; } }; // 处理单个媒体文件上传 const processMediaUpload = async (asset: MediaLibrary.Asset) => { try { // 检查权限 const { hasPermission } = await checkMediaLibraryPermission(); if (!hasPermission) { throw new Error('No media library permission'); } const isVideo = asset.mediaType === 'video'; // 上传原始文件 const uploadOriginalFile = async () => { const response = await fetch(asset.uri); const blob = await response.blob(); const filename = asset.filename || `${isVideo ? 'video' : 'image'}_${Date.now()}_original.${isVideo ? 'mp4' : 'jpg'}`; const mimeType = isVideo ? 'video/mp4' : 'image/jpeg'; const file = new File([blob], filename, { type: mimeType }); console.log("Original file prepared for upload:", file); // 获取上传URL并上传原始文件 const { upload_url, file_id } = await getUploadUrl(file, { originalUri: asset.uri, creationTime: asset.creationTime, mediaType: isVideo ? 'video' : 'image', isCompressed: false }); await uploadFile(file, upload_url); await confirmUpload(file_id); console.log(`Successfully uploaded original: ${filename}`); return { success: true, file_id }; }; // 上传压缩文件(仅图片) const uploadCompressedFile = async () => { if (isVideo) return { success: true, file_id: null }; // 视频不压缩 try { const { file: compressedFile } = await compressImage(asset.uri); const filename = asset.filename ? `compressed_${asset.filename}` : `image_${Date.now()}_compressed.jpg`; console.log("Compressed file prepared for upload:", { name: filename, size: compressedFile.size, type: compressedFile.type }); console.log("compressedFile", compressedFile); // 获取上传URL并上传压缩文件 const { upload_url, file_id } = await getUploadUrl(compressedFile, { originalUri: asset.uri, creationTime: asset.creationTime, mediaType: 'image', isCompressed: true }); await uploadFile(compressedFile, upload_url); await confirmUpload(file_id); console.log(`Successfully uploaded compressed: ${filename}`); return { success: true, file_id }; } catch (error) { console.error('Error uploading compressed file:', error); return { success: false, error }; } }; // 同时上传原始文件和压缩文件 console.log("Asset info:", asset); const [originalResult, compressedResult] = await Promise.all([ uploadOriginalFile(), uploadCompressedFile() ]); console.log("originalResult", originalResult); console.log("compressedResult", compressedResult); addMaterial(originalResult.file_id, compressedResult.file_id) return { originalSuccess: originalResult.success, compressedSuccess: compressedResult.success, fileIds: { original: originalResult.file_id, compressed: compressedResult.file_id } }; } catch (error: any) { console.error('Error in processMediaUpload:', error); if (error.message === 'No media library permission') { throw error; } return { originalSuccess: false, compressedSuccess: false, error }; } }; // 注册后台任务 export const registerBackgroundUploadTask = async () => { try { // 检查是否已注册 const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_UPLOAD_TASK); if (isRegistered) { await BackgroundFetch.unregisterTaskAsync(BACKGROUND_UPLOAD_TASK); } // 注册后台任务 await BackgroundFetch.registerTaskAsync(BACKGROUND_UPLOAD_TASK, { minimumInterval: 15 * 60, // 15分钟 stopOnTerminate: false, // 应用关闭后继续运行 startOnBoot: true, // 系统启动后自动运行 }); console.log('Background upload task registered'); return true; } catch (error) { console.error('Error registering background task:', error); return false; } }; // 定义后台任务 TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => { try { console.log('Background upload task started'); // 检查并请求媒体库权限 const { hasPermission } = await checkMediaLibraryPermission(); if (!hasPermission) { console.log('Media library permission not granted'); return BackgroundFetch.BackgroundFetchResult.NoData; } // 获取过去24小时内的媒体 const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const media = await getMediaByDateRange(yesterday, now); console.log("media", media); if (media.length === 0) { console.log('No media found in the specified time range'); return BackgroundFetch.BackgroundFetchResult.NoData; } console.log(`Found ${media.length} media items to process`); // 创建并发限制器 const limit = pLimit(CONCURRENCY_LIMIT); // 准备所有上传任务 const uploadPromises = media.map(item => limit(async () => { try { await processMediaUpload(item); return { success: true, id: item.id }; } catch (error: any) { console.error(`Error processing media ${item.id}:`, error); if (error.message === 'No media library permission') { throw error; // 权限错误直接抛出 } return { success: false, id: item.id, error }; } }) ); // 等待所有上传任务完成 const results = await Promise.allSettled(uploadPromises); // 统计结果 const succeeded = results.filter(r => r.status === 'fulfilled' && r.value.success ).length; const failed = results.length - succeeded; console.log(`Background upload task completed. Success: ${succeeded}, Failed: ${failed}`); // 如果有权限错误,返回失败 const hasPermissionError = results.some(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.error?.message === 'No media library permission') ); if (hasPermissionError) { return BackgroundFetch.BackgroundFetchResult.Failed; } return succeeded > 0 ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData; } catch (error) { console.error('Error in background upload task:', error); return BackgroundFetch.BackgroundFetchResult.Failed; } }); // 手动触发上传 export const triggerManualUpload = async (startDate: Date, endDate: Date) => { try { console.log('Starting manual upload...'); console.log("startDate", startDate); console.log("endDate", endDate); const media = await getMediaByDateRange(startDate, endDate); console.log("media", media); if (media.length === 0) { Alert.alert('提示', '在指定时间范围内未找到媒体文件'); return []; } const results = []; for (const item of media) { try { const result = await processMediaUpload(item); results.push({ id: item.id, originalSuccess: result.originalSuccess, compressedSuccess: result.compressedSuccess, fileIds: result.fileIds, }); } catch (error) { console.error(`Error uploading ${item.filename}:`, error); results.push({ id: item.id, originalSuccess: false, compressedSuccess: false, error: error instanceof Error ? error.message : 'Unknown error', }); } } const originalSuccessCount = results.filter(r => r.originalSuccess).length; const compressedSuccessCount = results.filter(r => r.compressedSuccess).length; Alert.alert('上传完成', `成功上传 ${originalSuccessCount}/${media.length} 个原始文件,${compressedSuccessCount}/${media.length} 个压缩文件`); return results; } catch (error) { console.error('Error in manual upload:', error); Alert.alert('错误', '上传过程中出现错误'); throw error; } };