memowake-front/components/file-upload/backgroundUploader.ts
2025-07-14 18:23:36 +08:00

443 lines
15 KiB
TypeScript

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<string, any> = {}): 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<void> => {
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;
}
};