239 lines
9.6 KiB
TypeScript
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);
|
|
});
|
|
}; |