From a91f493f02d14dec5654403470de1bf088fa9102 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Wed, 16 Jul 2025 20:32:43 +0800 Subject: [PATCH] chore --- app/(tabs)/index.tsx | 2 +- components/file-upload/autoUploadScreen.tsx | 5 +- components/file-upload/files-uploader.tsx | 18 +- lib/background-uploader/automatic.ts | 73 ++++++ lib/background-uploader/db.ts | 37 --- lib/background-uploader/fileProcessor.ts | 103 -------- lib/background-uploader/index.ts | 260 -------------------- lib/background-uploader/manual.ts | 41 +++ lib/background-uploader/task.ts | 79 ------ lib/background-uploader/uploader.ts | 158 +++++++++++- lib/db.ts | 30 +++ lib/video-process/videoThumbnail.ts | 24 +- 12 files changed, 330 insertions(+), 500 deletions(-) create mode 100644 lib/background-uploader/automatic.ts delete mode 100644 lib/background-uploader/db.ts delete mode 100644 lib/background-uploader/fileProcessor.ts delete mode 100644 lib/background-uploader/index.ts create mode 100644 lib/background-uploader/manual.ts delete mode 100644 lib/background-uploader/task.ts create mode 100644 lib/db.ts diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index ebc89a9..6a1272a 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,5 +1,5 @@ import { checkAuthStatus } from '@/lib/auth'; -import { registerBackgroundUploadTask } from '@/lib/background-uploader'; +import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic'; import * as MediaLibrary from 'expo-media-library'; import { useRouter } from 'expo-router'; import { useEffect, useState } from 'react'; diff --git a/components/file-upload/autoUploadScreen.tsx b/components/file-upload/autoUploadScreen.tsx index 02d7d90..57774d3 100644 --- a/components/file-upload/autoUploadScreen.tsx +++ b/components/file-upload/autoUploadScreen.tsx @@ -1,4 +1,5 @@ -import { registerBackgroundUploadTask, triggerManualUpload } from '@/lib/background-uploader'; +import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic'; +import { triggerManualUpload } from '@/lib/background-uploader/manual'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -14,7 +15,7 @@ export default function AutoUploadScreen() { setIsRegistered(registered); }; console.log("register background upload task"); - // registerTask(); + registerTask(); }, []); // 处理手动上传 diff --git a/components/file-upload/files-uploader.tsx b/components/file-upload/files-uploader.tsx index 2c6cc04..9ca2871 100644 --- a/components/file-upload/files-uploader.tsx +++ b/components/file-upload/files-uploader.tsx @@ -1,8 +1,8 @@ import { addMaterial, confirmUpload, getUploadUrl } from '@/lib/background-uploader/api'; -import { ConfirmUpload, FileUploadItem, UploadResult, UploadTask, ImagesuploaderProps, ExifData, defaultExifData } from '@/lib/background-uploader/types'; +import { ConfirmUpload, ExifData, FileUploadItem, ImagesuploaderProps, UploadResult, UploadTask, defaultExifData } from '@/lib/background-uploader/types'; import { uploadFileWithProgress } from '@/lib/background-uploader/uploader'; import { compressImage } from '@/lib/image-process/imageCompress'; -import * as ImageManipulator from 'expo-image-manipulator'; +import { createVideoThumbnailFile } from '@/lib/video-process/videoThumbnail'; import * as ImagePicker from 'expo-image-picker'; import * as Location from 'expo-location'; import * as MediaLibrary from 'expo-media-library'; @@ -95,18 +95,8 @@ export const ImagesUploader: React.FC = ({ { type: 'video/mp4' } ); - // 生成视频缩略图 - const thumbnailResult = await ImageManipulator.manipulateAsync( - asset.uri, - [{ resize: { width: 300 } }], - { compress: 0.7, format: ImageManipulator.SaveFormat.JPEG } - ); - - thumbnailFile = new File( - [await (await fetch(thumbnailResult.uri)).blob()], - `thumb_${Date.now()}.jpg`, - { type: 'image/jpeg' } - ); + // 使用复用函数生成视频缩略图 + thumbnailFile = await createVideoThumbnailFile(asset, 300); } else { // 处理图片,主图和缩略图都用 compressImage 方法 // 主图压缩(按 maxWidth/maxHeight/compressQuality) diff --git a/lib/background-uploader/automatic.ts b/lib/background-uploader/automatic.ts new file mode 100644 index 0000000..a548663 --- /dev/null +++ b/lib/background-uploader/automatic.ts @@ -0,0 +1,73 @@ +import * as BackgroundTask from 'expo-background-task'; +import * as TaskManager from 'expo-task-manager'; +import { initUploadTable } from '../db'; +import { getMediaByDateRange } from './media'; +import { processAndUploadMedia } from './uploader'; + +const BACKGROUND_UPLOAD_TASK = 'background-upload-task'; + +// 注册后台任务 +export const registerBackgroundUploadTask = async () => { + try { + // 初始化数据库表 + await initUploadTable(); + + const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_UPLOAD_TASK); + if (isRegistered) { + console.log('Background task already registered.'); + } else { + await BackgroundTask.registerTaskAsync(BACKGROUND_UPLOAD_TASK, { + minimumInterval: 15 * 60, // 15 分钟 + }); + console.log('Background task registered successfully.'); + } + return true; + } catch (error) { + console.error('Error registering background task:', error); + return false; + } +}; + +// 定义后台任务 +TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => { + try { + console.log('Running background upload task...'); + const now = new Date(); + const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + + // 获取最近24小时的媒体文件 + const media = await getMediaByDateRange(oneDayAgo, now); + + if (media.length === 0) { + console.log('No new media files to upload in the last 24 hours.'); + return BackgroundTask.BackgroundTaskResult.Success; + } + + console.log(`Found ${media.length} media files to potentially upload.`); + + // 串行上传文件 + let successCount = 0; + let skippedCount = 0; + + for (const file of media) { + try { + const result = await processAndUploadMedia(file); + if (result === null) { + // 文件已上传,被跳过 + skippedCount++; + } else if (result.originalSuccess) { + successCount++; + } + } catch (e) { + console.error('Upload failed for', file.uri, e); + } + } + + console.log(`Background upload task finished. Successful: ${successCount}, Skipped: ${skippedCount}, Total: ${media.length}`); + + return BackgroundTask.BackgroundTaskResult.Success; + } catch (error) { + console.error('Background task error:', error); + return BackgroundTask.BackgroundTaskResult.Failed; + } +}); \ No newline at end of file diff --git a/lib/background-uploader/db.ts b/lib/background-uploader/db.ts deleted file mode 100644 index e850c52..0000000 --- a/lib/background-uploader/db.ts +++ /dev/null @@ -1,37 +0,0 @@ -// import * as SQLite from 'expo-sqlite'; - -// const db = SQLite.openDatabase('upload_status.db'); - -// // 初始化表 -// export function initUploadTable() { -// db.transaction(tx => { -// tx.executeSql( -// `CREATE TABLE IF NOT EXISTS uploaded_files ( -// uri TEXT PRIMARY KEY NOT NULL -// );` -// ); -// }); -// } - -// // 检查文件是否已上传 -// export function isFileUploaded(uri: string): Promise { -// return new Promise(resolve => { -// db.transaction(tx => { -// tx.executeSql( -// 'SELECT uri FROM uploaded_files WHERE uri = ?;', -// [uri], -// (_, { rows }) => resolve(rows.length > 0) -// ); -// }); -// }); -// } - -// // 记录文件已上传 -// export function markFileAsUploaded(uri: string) { -// db.transaction(tx => { -// tx.executeSql( -// 'INSERT OR IGNORE INTO uploaded_files (uri) VALUES (?);', -// [uri] -// ); -// }); -// } diff --git a/lib/background-uploader/fileProcessor.ts b/lib/background-uploader/fileProcessor.ts deleted file mode 100644 index eca45c9..0000000 --- a/lib/background-uploader/fileProcessor.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as FileSystem from 'expo-file-system'; -import { confirmUpload, getUploadUrl } from './api'; -import { ExtendedAsset } from './types'; -import { uploadFile } from './uploader'; - -// 将 HEIC 图片转化 -export const convertHeicToJpeg = async (uri: string): Promise => { - try { - console.log('Starting HEIC to JPEG conversion for:', uri); - - // 1. 将文件复制到缓存目录 - const cacheDir = FileSystem.cacheDirectory; - if (!cacheDir) { - throw new Error('Cache directory not available'); - } - - // 创建唯一的文件名 - const tempUri = `${cacheDir}${Date.now()}.heic`; - - // 复制文件到缓存目录 - await FileSystem.copyAsync({ - from: uri, - to: tempUri - }); - - // 2. 检查文件是否存在 - const fileInfo = await FileSystem.getInfoAsync(tempUri); - if (!fileInfo.exists) { - throw new Error('Temporary file was not created'); - } - - // 3. 读取文件为 base64 - const base64 = await FileSystem.readAsStringAsync(tempUri, { - encoding: FileSystem.EncodingType.Base64, - }); - - if (!base64) { - throw new Error('Failed to read file as base64'); - } - - - - // 5. 创建文件名 - const originalName = uri.split('/').pop() || 'converted'; - const filename = originalName.replace(/\.(heic|heif)$/i, '.jpg'); - - console.log('Successfully converted HEIC to JPEG:', filename); - - // 清理临时文件 - try { - await FileSystem.deleteAsync(tempUri, { idempotent: true }); - } catch (cleanupError) { - console.warn('Failed to clean up temporary file:', cleanupError); - } - - return new File([blob], filename, { type: 'image/jpeg' }); - } catch (error: unknown) { - console.error('Detailed HEIC conversion error:', { - error: error instanceof Error ? { - message: error.message, - name: error.name, - stack: error.stack - } : error, - uri: uri - }); - throw new Error(`Failed to convert HEIC image: ${error instanceof Error ? error.message : 'An unknown error occurred'}`); - } -}; - -// 压缩图片 -import { compressImage } from '../image-process/imageCompress'; - -// 提取视频的首帧进行压缩并上传 -export const uploadVideoThumbnail = async (asset: ExtendedAsset) => { - 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 uploadFile(compressedFile, upload_url); - await confirmUpload(file_id); - - console.log('视频首帧文件上传成功:', { - fileId: file_id, - filename: compressedFile.name, - type: compressedFile.type - }); - return { success: true, file_id }; - } catch (error) { - return { success: false, error }; - } -}; \ No newline at end of file diff --git a/lib/background-uploader/index.ts b/lib/background-uploader/index.ts deleted file mode 100644 index e317411..0000000 --- a/lib/background-uploader/index.ts +++ /dev/null @@ -1,260 +0,0 @@ -import pLimit from 'p-limit'; -import { Alert } from 'react-native'; -import { transformData } from '@/components/utils/objectFlat'; -import { ExtendedAsset } from './types'; -import { getMediaByDateRange } from './media'; -import { checkMediaLibraryPermission, getFileExtension, getMimeType } from './utils'; -import { convertHeicToJpeg, uploadVideoThumbnail } from './fileProcessor'; -import { compressImage } from '../image-process/imageCompress'; -import { getUploadUrl, confirmUpload, addMaterial } from './api'; -import { uploadFile } from './uploader'; -import * as MediaLibrary from 'expo-media-library'; - -// 设置最大并发数 -const CONCURRENCY_LIMIT = 10; // 同时最多上传10个文件 -const limit = pLimit(CONCURRENCY_LIMIT); - -// 处理单个媒体文件上传 -export const processMediaUpload = async (asset: ExtendedAsset) => { - try { - // 检查权限 - const { hasPermission } = await checkMediaLibraryPermission(); - if (!hasPermission) { - throw new Error('No media library permission'); - } - - const isVideo = asset.mediaType === 'video'; - - // 上传原始文件 - const uploadOriginalFile = async () => { - try { - let fileToUpload: File; - const isVideo = asset.mediaType === 'video'; - 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 }); - console.log('文件准备上传:', { - name: fileToUpload.name, - type: fileToUpload.type, - size: fileToUpload.size - }); - } - - // 准备元数据 - let exifData = {}; - if (asset.exif) { - try { - exifData = transformData({ - ...asset, - exif: { - ...asset.exif, - '{MakerApple}': undefined - } - }); - } catch (exifError) { - console.warn('处理 EXIF 数据时出错:', exifError); - } - } - - // 获取上传 URL - const { upload_url, file_id } = await getUploadUrl(fileToUpload, { - originalUri: asset.uri, - creationTime: asset.creationTime, - mediaType: isVideo ? 'video' : 'image', - isCompressed: false, - ...exifData, - GPSVersionID: undefined - }); - - // 上传文件 - await uploadFile(fileToUpload, upload_url); - await confirmUpload(file_id); - - console.log('文件上传成功:', { - fileId: file_id, - filename: fileToUpload.name, - type: fileToUpload.type - }); - - return { - success: true, - file_id, - filename: fileToUpload.name - }; - - } catch (error: any) { - const errorMessage = error instanceof Error ? error.message : '未知错误'; - console.error('上传原始文件时出错:', { - error: errorMessage, - assetId: asset.id, - filename: asset.filename, - uri: asset.uri - }); - throw new Error(`上传失败: ${errorMessage}`); - } - }; - - // 上传压缩文件(仅图片) - 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 uploadFile(compressedFile, upload_url); - await confirmUpload(file_id); - return { success: true, file_id }; - } catch (error) { - return { success: false, error }; - } - }; - - // 先上传原始文件 - const originalResult = await uploadOriginalFile(); - - // 如果是图片,再上传压缩文件 - let compressedResult: { success: boolean; file_id?: string | null; error?: any } = { success: true, file_id: null }; - if (!isVideo) { - compressedResult = await uploadCompressedFile(); - // 添加素材 - addMaterial(originalResult.file_id, compressedResult?.file_id || ''); - } else { - // 上传压缩首帧 - const thumbnailResult = await uploadVideoThumbnail(asset); - if (thumbnailResult.success) { - addMaterial(originalResult.file_id, thumbnailResult.file_id || ''); - } - } - - return { - originalSuccess: originalResult.success, - compressedSuccess: compressedResult.success, - fileIds: { - original: originalResult.file_id, - compressed: compressedResult.file_id - } - }; - } catch (error: any) { - if (error.message === 'No media library permission') { - throw error; - } - return { - originalSuccess: false, - compressedSuccess: false, - error: error.message - }; - } -}; - -// 手动触发上传 -export const triggerManualUpload = async (startDate: Date, endDate: Date) => { - try { - const media = await getMediaByDateRange(startDate, endDate); - if (media.length === 0) { - Alert.alert('提示', '在指定时间范围内未找到媒体文件'); - return []; - } - - // 分离图片和视频 - const photos = media.filter(item => item.mediaType === 'photo'); - const videos = media.filter(item => item.mediaType === 'video'); - console.log('videos11111111', videos); - - const results: any[] = []; - - // 处理所有图片(带并发控制) - const processPhoto = async (item: any) => { - try { - const result = await processMediaUpload(item); - results.push({ - id: item.id, - ...result - }); - } catch (error: any) { - results.push({ - id: item.id, - originalSuccess: false, - compressedSuccess: false, - error: error.message - }); - } - }; - - // 处理所有视频(带并发控制) - const processVideo = async (item: any) => { - try { - const result = await processMediaUpload(item); - results.push({ - id: item.id, - ...result - }); - } catch (error: any) { - results.push({ - id: item.id, - originalSuccess: false, - compressedSuccess: false, - error: error.message - }); - } - }; - - // 并发处理图片和视频 - await Promise.all([ - ...photos.map(photo => limit(() => processPhoto(photo))), - ...videos.map(video => limit(() => processVideo(video))) - ]); - - return results; - } catch (error) { - Alert.alert('错误', '上传过程中出现错误'); - throw error; - } -}; - -export { registerBackgroundUploadTask } from './task'; diff --git a/lib/background-uploader/manual.ts b/lib/background-uploader/manual.ts new file mode 100644 index 0000000..1e9638b --- /dev/null +++ b/lib/background-uploader/manual.ts @@ -0,0 +1,41 @@ +import { Alert } from 'react-native'; +import pLimit from 'p-limit'; +import { getMediaByDateRange } from './media'; +import { processAndUploadMedia } from './uploader'; +import { ExtendedAsset } from './types'; + +// 设置最大并发数 +const CONCURRENCY_LIMIT = 10; // 同时最多上传10个文件 +const limit = pLimit(CONCURRENCY_LIMIT); + +// 手动触发上传 +export const triggerManualUpload = async (startDate: Date, endDate: Date) => { + try { + const media = await getMediaByDateRange(startDate, endDate); + if (media.length === 0) { + Alert.alert('提示', '在指定时间范围内未找到媒体文件'); + return []; + } + + const uploadPromises = media.map((asset: ExtendedAsset) => + limit(() => processAndUploadMedia(asset)) + ); + + const results = await Promise.all(uploadPromises); + + // 过滤掉因为已上传而返回 null 的结果 + const finalResults = results.filter(result => result !== null); + + console.log('Manual upload completed.', { + total: media.length, + uploaded: finalResults.length, + skipped: media.length - finalResults.length + }); + + return finalResults; + } catch (error) { + console.error('手动上传过程中出现错误:', error); + Alert.alert('错误', '上传过程中出现错误'); + throw error; + } +}; \ No newline at end of file diff --git a/lib/background-uploader/task.ts b/lib/background-uploader/task.ts deleted file mode 100644 index 0e242c4..0000000 --- a/lib/background-uploader/task.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as BackgroundFetch from 'expo-background-task'; -import * as TaskManager from 'expo-task-manager'; -import { isFileUploaded, markFileAsUploaded } from './db'; -import { getMediaByDateRange } from './media'; - -const BACKGROUND_UPLOAD_TASK = 'background-upload-task'; - -// 注册后台任务 -export const registerBackgroundUploadTask = async () => { - try { - // 初始化数据库表 - // initUploadTable(); - // 检查是否已经注册了任务 - 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 分钟 - }); - - console.log('Background task registered'); - return true; - } catch (error) { - console.error('Error registering background task:', error); - return false; - } -}; - -// 定义后台任务 -TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => { - try { - const now = new Date(); - const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - - // 获取最近24小时的媒体文件 - const media = await getMediaByDateRange(oneDayAgo, now); - - // 过滤已上传文件(以 uri 为唯一标识) - const filesToUpload = []; - for (const file of media) { - const uploaded = await isFileUploaded(file.uri); - if (!uploaded) filesToUpload.push(file); - } - - if (filesToUpload.length === 0) { - console.log('No media files to upload'); - return BackgroundFetch.BackgroundTaskResult.Success; - } - - // 上传未上传文件 - let successCount = 0; - for (const file of filesToUpload) { - // 这里假设有 uploadSingleMedia 函数,或直接使用 index.ts 的 processMediaUpload - try { - // 你可以根据实际情况替换为自己的上传逻辑 - const { processMediaUpload } = await import('./index'); - const result = await processMediaUpload(file); - if (result.originalSuccess) { - markFileAsUploaded(file.uri); - successCount++; - } - } catch (e) { - console.error('Upload failed for', file.uri, e); - } - } - - console.log(`Background upload completed. Success: ${successCount}/${filesToUpload.length}`); - - return successCount > 0 - ? BackgroundFetch.BackgroundTaskResult.Success - : BackgroundFetch.BackgroundTaskResult.Failed; - } catch (error) { - console.error('Background task error:', error); - return BackgroundFetch.BackgroundTaskResult.Failed; - } -}); \ No newline at end of file diff --git a/lib/background-uploader/uploader.ts b/lib/background-uploader/uploader.ts index 265b4cc..09874bd 100644 --- a/lib/background-uploader/uploader.ts +++ b/lib/background-uploader/uploader.ts @@ -1,4 +1,14 @@ -// 上传文件到URL(基础版,无进度回调) +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 { isFileUploaded, markFileAsUploaded } from '../db'; + +// 基础文件上传实现 export const uploadFile = async (file: File, uploadUrl: string): Promise => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); @@ -18,7 +28,151 @@ export const uploadFile = async (file: File, uploadUrl: string): Promise = }); }; -// 支持进度回调和超时的上传实现 + +// 处理单个媒体文件上传的核心逻辑 +export const processAndUploadMedia = async (asset: ExtendedAsset) => { + try { + // 1. 文件去重检查 + const uploaded = await isFileUploaded(asset.uri); + if (uploaded) { + console.log('File already uploaded, skipping:', asset.uri); + return null; // 返回 null 表示已上传,调用方可以据此过滤 + } + + // 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 uploadFile(fileToUpload, upload_url); + 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 uploadFile(compressedFile, upload_url); + 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) { + addMaterial(originalResult.file_id, compressedResult.file_id); + } + } else { + const thumbnailResult = await uploadVideoThumbnail(asset); + if (thumbnailResult.success && originalResult.file_id && thumbnailResult.file_id) { + addMaterial(originalResult.file_id, thumbnailResult.file_id); + } + } + + // 5. 标记为已上传 + await markFileAsUploaded(asset.uri); + + 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); + return { + id: asset.id, + originalSuccess: false, + compressedSuccess: false, + error: error.message + }; + } +}; export const uploadFileWithProgress = async ( file: File, uploadUrl: string, diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 0000000..44ed2ce --- /dev/null +++ b/lib/db.ts @@ -0,0 +1,30 @@ +import * as SQLite from 'expo-sqlite'; + +const db = SQLite.openDatabaseSync('upload_status.db'); + +// 初始化表 +export function initUploadTable() { + console.log('Initializing upload table...'); + db.execSync(` + CREATE TABLE IF NOT EXISTS uploaded_files ( + uri TEXT PRIMARY KEY NOT NULL + ); + `); + console.log('Upload table initialized'); +} + +// 检查文件是否已上传 (使用同步API,但保持接口为Promise以减少外部重构) +export async function isFileUploaded(uri: string): Promise { + console.log('Checking if file is uploaded:', uri) + const result = db.getFirstSync<{ uri: string }>( + 'SELECT uri FROM uploaded_files WHERE uri = ?;', + uri + ); + console.log('File uploaded result:', result) + return !!result; +} + +// 记录文件已上传 +export function markFileAsUploaded(uri: string) { + db.runSync('INSERT OR IGNORE INTO uploaded_files (uri) VALUES (?);', uri); +} diff --git a/lib/video-process/videoThumbnail.ts b/lib/video-process/videoThumbnail.ts index f8632a8..3803e60 100644 --- a/lib/video-process/videoThumbnail.ts +++ b/lib/video-process/videoThumbnail.ts @@ -1,7 +1,27 @@ -import { getUploadUrl, confirmUpload } from '../background-uploader/api'; +import * as ImageManipulator from 'expo-image-manipulator'; +import { confirmUpload, getUploadUrl } from '../background-uploader/api'; +import { ExtendedAsset } from '../background-uploader/types'; import { uploadFile } from '../background-uploader/uploader'; import { compressImage } from '../image-process/imageCompress'; -import { ExtendedAsset } from '../background-uploader/types'; + +/** + * @description 从视频资源创建缩略图文件 + * @param asset 媒体资源 + * @param width 缩略图宽度,默认为 300 + * @returns 返回一个包含缩略图的 File 对象 + */ +export const createVideoThumbnailFile = async (asset: { uri: string }, width: number = 300): Promise => { + const thumbnailResult = await ImageManipulator.manipulateAsync( + asset.uri, + [{ resize: { width } }], + { compress: 0.7, format: ImageManipulator.SaveFormat.WEBP } + ); + + const response = await fetch(thumbnailResult.uri); + const blob = await response.blob(); + + return new File([blob], `thumb_${Date.now()}.webp`, { type: 'image/webp' }); +}; // 提取视频的首帧进行压缩并上传 export const uploadVideoThumbnail = async (asset: ExtendedAsset) => {