memowake-front/components/file-upload/backgroundUploader.ts
2025-07-14 16:57:24 +08:00

384 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { Alert } from 'react-native';
const BACKGROUND_UPLOAD_TASK = 'background-upload-task';
// 获取指定时间范围内的媒体文件
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> => {
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);
});
};
// 检查并请求媒体库权限
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:", {
name: file.name,
size: file.size,
type: file.type
});
// 获取上传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
});
// 获取上传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) {
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');
// 返回 NoData 而不是 Failed这样系统可能会在稍后重试
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`);
// 处理每个媒体文件
for (const item of media) {
try {
await processMediaUpload(item);
} catch (error) {
console.error(`Error processing media ${item.id}:`, error);
// 如果是权限错误,直接返回失败
if (error.message === 'No media library permission') {
return BackgroundFetch.BackgroundFetchResult.Failed;
}
// 其他错误继续处理下一个文件
}
}
console.log('Background upload task completed');
return BackgroundFetch.BackgroundFetchResult.NewData;
} 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;
}
};