239 lines
9.6 KiB
TypeScript

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 { getUploadTaskStatus, updateUploadTaskStatus, updateUploadTaskProgress } from '../db';
// 基础文件上传实现
export 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);
});
};
// 处理单个媒体文件上传的核心逻辑
export const processAndUploadMedia = async (
asset: ExtendedAsset,
onProgress?: (progress: { loaded: number; total: number }) => void
) => {
try {
// 1. 文件去重检查 (从数据库获取状态)
const existingTask = await getUploadTaskStatus(asset.uri);
if (existingTask && (existingTask.status === 'success' || existingTask.status === 'skipped')) {
console.log(`File ${asset.uri} already ${existingTask.status}, skipping processing.`);
return null; // 返回 null 表示已上传或已跳过,调用方可以据此过滤
}
// 标记为正在上传
await updateUploadTaskStatus(asset.uri, 'uploading');
// 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 uploadFileWithProgress(fileToUpload, upload_url, (progress) => {
if (onProgress) onProgress(progress);
const percentage = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0;
updateUploadTaskProgress(asset.uri, Math.round(percentage * 0.5)); // 原始文件占总进度的50%
});
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 uploadFileWithProgress(compressedFile, upload_url, (progress) => {
// For compressed files, we can't easily report byte progress relative to the whole process,
// as we don't know the compressed size in advance. We'll just update the DB progress.
const percentage = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0;
updateUploadTaskProgress(asset.uri, 50 + Math.round(percentage * 0.5)); // 压缩文件占总进度的后50%
});
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);
}
}
// 标记为已上传
await updateUploadTaskStatus(asset.uri, 'success', originalResult.file_id);
await updateUploadTaskProgress(asset.uri, 100);
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);
// 标记为失败
await updateUploadTaskStatus(asset.uri, 'failed');
await updateUploadTaskProgress(asset.uri, 0);
return {
id: asset.id,
originalSuccess: false,
compressedSuccess: false,
error: error.message
};
}
};
export const uploadFileWithProgress = async (
file: File,
uploadUrl: string,
onProgress?: (progress: { loaded: number; total: number }) => void,
timeout: number = 30000
): Promise<void> => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
let timeoutId: number | undefined;
xhr.open('PUT', uploadUrl);
xhr.setRequestHeader('Content-Type', file.type);
// 进度监听
if (onProgress) {
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
onProgress({ loaded: event.loaded, total: event.total });
}
};
}
xhr.onload = () => {
clearTimeout(timeoutId);
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
xhr.onerror = () => {
clearTimeout(timeoutId);
reject(new Error('Network error during upload'));
};
// 超时处理
timeoutId = setTimeout(() => {
xhr.abort();
reject(new Error('上传超时,请检查网络连接'));
}, timeout);
xhr.send(file);
});
};