chore
This commit is contained in:
parent
f63497f3a1
commit
a65f88e9a9
@ -1,4 +1,4 @@
|
||||
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 * as SecureStore from 'expo-secure-store';
|
||||
|
||||
@ -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();
|
||||
}, []);
|
||||
|
||||
// 处理手动上传
|
||||
|
||||
@ -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<ImagesuploaderProps> = ({
|
||||
{ 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)
|
||||
|
||||
73
lib/background-uploader/automatic.ts
Normal file
73
lib/background-uploader/automatic.ts
Normal file
@ -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;
|
||||
}
|
||||
});
|
||||
@ -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<boolean> {
|
||||
// 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]
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
@ -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<File> => {
|
||||
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 };
|
||||
}
|
||||
};
|
||||
@ -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';
|
||||
41
lib/background-uploader/manual.ts
Normal file
41
lib/background-uploader/manual.ts
Normal file
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -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<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
@ -18,7 +28,151 @@ export const uploadFile = async (file: File, uploadUrl: string): Promise<void> =
|
||||
});
|
||||
};
|
||||
|
||||
// 支持进度回调和超时的上传实现
|
||||
|
||||
// 处理单个媒体文件上传的核心逻辑
|
||||
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,
|
||||
|
||||
30
lib/db.ts
Normal file
30
lib/db.ts
Normal file
@ -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<boolean> {
|
||||
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);
|
||||
}
|
||||
@ -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<File> => {
|
||||
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) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user