This commit is contained in:
Junhui Chen 2025-07-16 20:32:43 +08:00
parent f63497f3a1
commit a65f88e9a9
12 changed files with 330 additions and 500 deletions

View File

@ -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';

View File

@ -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();
}, []);
// 处理手动上传

View File

@ -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

View 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;
}
});

View File

@ -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]
// );
// });
// }

View File

@ -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 };
}
};

View File

@ -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';

View 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;
}
};

View File

@ -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;
}
});

View File

@ -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
View 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);
}

View File

@ -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) => {