diff --git a/lib/database/database-factory.ts b/lib/database/database-factory.ts new file mode 100644 index 0000000..abf4f47 --- /dev/null +++ b/lib/database/database-factory.ts @@ -0,0 +1,23 @@ +import { DatabaseInterface } from './types'; +import { SQLiteDatabase } from './sqlite-database'; + +class DatabaseFactory { + private static instance: DatabaseInterface | null = null; + + static getInstance(): DatabaseInterface { + if (!this.instance) { + // Metro 会根据平台自动选择正确的文件 + // Web: sqlite-database.web.ts + // Native: sqlite-database.ts + this.instance = new SQLiteDatabase(); + } + return this.instance!; + } + + // 用于测试或重置实例 + static resetInstance(): void { + this.instance = null; + } +} + +export const database = DatabaseFactory.getInstance(); diff --git a/lib/database/database-test.ts b/lib/database/database-test.ts new file mode 100644 index 0000000..fc1beb2 --- /dev/null +++ b/lib/database/database-test.ts @@ -0,0 +1,60 @@ +// 数据库架构测试文件 +import { database } from './database-factory'; +import { UploadTask } from './types'; + +// 测试数据库基本功能 +export async function testDatabase() { + console.log('开始测试数据库功能...'); + + try { + // 初始化数据库 + await database.initUploadTable(); + console.log('✓ 数据库初始化成功'); + + // 测试插入任务 + const testTask: Omit = { + uri: 'test://example.jpg', + filename: 'example.jpg', + status: 'pending', + progress: 0, + file_id: undefined + }; + + await database.insertUploadTask(testTask); + console.log('✓ 任务插入成功'); + + // 测试查询任务 + const retrievedTask = await database.getUploadTaskStatus(testTask.uri); + if (retrievedTask) { + console.log('✓ 任务查询成功:', retrievedTask); + } else { + console.log('✗ 任务查询失败'); + } + + // 测试更新任务状态 + await database.updateUploadTaskStatus(testTask.uri, 'success', 'file123'); + console.log('✓ 任务状态更新成功'); + + // 测试获取所有任务 + const allTasks = await database.getUploadTasks(); + console.log('✓ 获取所有任务成功,数量:', allTasks.length); + + // 测试应用状态 + await database.setAppState('test_key', 'test_value'); + const stateValue = await database.getAppState('test_key'); + console.log('✓ 应用状态测试成功:', stateValue); + + // 清理测试数据 + await database.cleanUpUploadTasks(); + console.log('✓ 数据清理成功'); + + console.log('🎉 所有数据库测试通过!'); + + } catch (error) { + console.error('❌ 数据库测试失败:', error); + throw error; + } +} + +// 导出测试函数供调用 +export default testDatabase; diff --git a/lib/database/empty-sqlite.js b/lib/database/empty-sqlite.js new file mode 100644 index 0000000..aa6e3fc --- /dev/null +++ b/lib/database/empty-sqlite.js @@ -0,0 +1,9 @@ +// 空的 SQLite 模块,用于 Web 环境 +console.warn('SQLite is not available in web environment'); + +// 导出空的对象,避免导入错误 +module.exports = { + openDatabaseSync: () => { + throw new Error('SQLite is not available in web environment'); + } +}; diff --git a/lib/database/index.ts b/lib/database/index.ts new file mode 100644 index 0000000..1c5595f --- /dev/null +++ b/lib/database/index.ts @@ -0,0 +1,6 @@ +// 数据库模块统一导出 +export { DatabaseInterface, UploadTask } from './types'; +export { WebDatabase } from './web-database'; +export { SQLiteDatabase } from './sqlite-database'; +export { database } from './database-factory'; +export { testDatabase } from './database-test'; diff --git a/lib/database/sqlite-database.ts b/lib/database/sqlite-database.ts new file mode 100644 index 0000000..9782d6c --- /dev/null +++ b/lib/database/sqlite-database.ts @@ -0,0 +1,156 @@ +import { DatabaseInterface, UploadTask } from './types'; + +export class SQLiteDatabase implements DatabaseInterface { + private db: any; + + constructor() { + // 动态导入,避免在Web环境下加载 + try { + const SQLite = require('expo-sqlite'); + this.db = SQLite.openDatabaseSync('upload_status.db'); + this.db.execSync('PRAGMA busy_timeout = 5000;'); + } catch (error) { + console.error('Failed to initialize SQLite:', error); + throw new Error('SQLite is not available in this environment'); + } + } + + async initUploadTable(): Promise { + console.log('Initializing upload tasks table...'); + await this.db.execAsync(` + CREATE TABLE IF NOT EXISTS upload_tasks ( + uri TEXT PRIMARY KEY NOT NULL, + filename TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + progress INTEGER NOT NULL DEFAULT 0, + file_id TEXT, + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + ); + `); + + // Add created_at column to existing table if it doesn't exist + const columns = await this.db.getAllAsync('PRAGMA table_info(upload_tasks);'); + const columnExists = columns.some((column: any) => column.name === 'created_at'); + + if (!columnExists) { + console.log('Adding created_at column to upload_tasks table...'); + await this.db.execAsync(`ALTER TABLE upload_tasks ADD COLUMN created_at INTEGER;`); + await this.db.execAsync(`UPDATE upload_tasks SET created_at = (strftime('%s', 'now')) WHERE created_at IS NULL;`); + console.log('created_at column added and populated.'); + } + console.log('Upload tasks table initialized'); + + await this.db.execAsync(` + CREATE TABLE IF NOT EXISTS app_state ( + key TEXT PRIMARY KEY NOT NULL, + value TEXT + ); + `); + console.log('App state table initialized'); + } + + async insertUploadTask(task: Omit): Promise { + console.log('Inserting upload task:', task.uri); + await this.db.runAsync( + 'INSERT OR REPLACE INTO upload_tasks (uri, filename, status, progress, file_id, created_at) VALUES (?, ?, ?, ?, ?, ?)', + [task.uri, task.filename, task.status, task.progress, task.file_id ?? null, Math.floor(Date.now() / 1000)] + ); + } + + async getUploadTaskStatus(uri: string): Promise { + console.log('Checking upload task status for:', uri); + const result = await this.db.getFirstAsync( + 'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks WHERE uri = ?;', + uri + ); + return result || null; + } + + async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise { + if (file_id) { + await this.db.runAsync('UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?', [status, file_id, uri]); + } else { + await this.db.runAsync('UPDATE upload_tasks SET status = ? WHERE uri = ?', [status, uri]); + } + } + + async updateUploadTaskProgress(uri: string, progress: number): Promise { + await this.db.runAsync('UPDATE upload_tasks SET progress = ? WHERE uri = ?', [progress, uri]); + } + + async getUploadTasks(): Promise { + console.log('Fetching all upload tasks... time:', new Date().toLocaleString()); + const results = await this.db.getAllAsync( + 'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks ORDER BY created_at DESC;' + ); + return results; + } + + async cleanUpUploadTasks(): Promise { + console.log('Cleaning up completed/failed upload tasks...'); + await this.db.runAsync( + "DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';" + ); + } + + async getUploadTasksSince(timestamp: number): Promise { + const rows = await this.db.getAllAsync( + 'SELECT * FROM upload_tasks WHERE created_at >= ? ORDER BY created_at DESC', + [timestamp] + ); + return rows; + } + + async exist_pending_tasks(): Promise { + const rows = await this.db.getAllAsync( + 'SELECT * FROM upload_tasks WHERE status = "pending" OR status = "uploading"' + ); + return rows.length > 0; + } + + async filterExistingFiles(fileUris: string[]): Promise { + if (fileUris.length === 0) { + return []; + } + + const placeholders = fileUris.map(() => '?').join(','); + const query = `SELECT uri FROM upload_tasks WHERE uri IN (${placeholders}) AND status = 'success'`; + + const existingFiles = await this.db.getAllAsync(query, fileUris); + const existingUris = new Set(existingFiles.map((f: { uri: string }) => f.uri)); + const newFileUris = fileUris.filter(uri => !existingUris.has(uri)); + + console.log(`[DB] Total files: ${fileUris.length}, Existing successful files: ${existingUris.size}, New files to upload: ${newFileUris.length}`); + + return newFileUris; + } + + async setAppState(key: string, value: string | null): Promise { + console.log(`Setting app state: ${key} = ${value}`); + await this.db.runAsync('INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)', [key, value]); + } + + async getAppState(key: string): Promise { + const result = await this.db.getFirstAsync('SELECT value FROM app_state WHERE key = ?;', key); + return result?.value ?? null; + } + + async executeSql(sql: string, params: any[] = []): Promise { + try { + const isSelect = sql.trim().toLowerCase().startsWith('select'); + if (isSelect) { + const results = this.db.getAllSync(sql, params); + return results; + } else { + const result = this.db.runSync(sql, params); + return { + changes: result.changes, + lastInsertRowId: result.lastInsertRowId, + }; + } + } catch (error: any) { + console.error("Error executing SQL:", error); + return { error: error.message }; + } + } +} diff --git a/lib/database/sqlite-database.web.ts b/lib/database/sqlite-database.web.ts new file mode 100644 index 0000000..3b3ba1a --- /dev/null +++ b/lib/database/sqlite-database.web.ts @@ -0,0 +1,140 @@ +import { DatabaseInterface, UploadTask } from './types'; + +// Web 环境下的 SQLite 数据库实现(实际使用 localStorage) +export class SQLiteDatabase implements DatabaseInterface { + private getStorageKey(table: string): string { + return `memowake_${table}`; + } + + private getUploadTasksFromStorage(): UploadTask[] { + const data = localStorage.getItem(this.getStorageKey('upload_tasks')); + return data ? JSON.parse(data) : []; + } + + private saveUploadTasks(tasks: UploadTask[]): void { + localStorage.setItem(this.getStorageKey('upload_tasks'), JSON.stringify(tasks)); + } + + private getAppStateData(): Record { + const data = localStorage.getItem(this.getStorageKey('app_state')); + return data ? JSON.parse(data) : {}; + } + + private saveAppStateData(state: Record): void { + localStorage.setItem(this.getStorageKey('app_state'), JSON.stringify(state)); + } + + async initUploadTable(): Promise { + console.log('Initializing web storage tables (SQLite fallback)...'); + // Web端不需要初始化表结构,localStorage会自动处理 + } + + async insertUploadTask(task: Omit): Promise { + console.log('Inserting upload task:', task.uri); + const tasks = this.getUploadTasksFromStorage(); + const existingIndex = tasks.findIndex(t => t.uri === task.uri); + const newTask: UploadTask = { + ...task, + created_at: Math.floor(Date.now() / 1000) + }; + + if (existingIndex >= 0) { + tasks[existingIndex] = newTask; + } else { + tasks.push(newTask); + } + + this.saveUploadTasks(tasks); + } + + async getUploadTaskStatus(uri: string): Promise { + console.log('Checking upload task status for:', uri); + const tasks = this.getUploadTasksFromStorage(); + return tasks.find(t => t.uri === uri) || null; + } + + async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise { + const tasks = this.getUploadTasksFromStorage(); + const taskIndex = tasks.findIndex(t => t.uri === uri); + if (taskIndex >= 0) { + tasks[taskIndex].status = status; + if (file_id) { + tasks[taskIndex].file_id = file_id; + } + this.saveUploadTasks(tasks); + } + } + + async updateUploadTaskProgress(uri: string, progress: number): Promise { + const tasks = this.getUploadTasksFromStorage(); + const taskIndex = tasks.findIndex(t => t.uri === uri); + if (taskIndex >= 0) { + tasks[taskIndex].progress = progress; + this.saveUploadTasks(tasks); + } + } + + async getUploadTasks(): Promise { + console.log('Fetching all upload tasks... time:', new Date().toLocaleString()); + const tasks = this.getUploadTasksFromStorage(); + return tasks.sort((a, b) => b.created_at - a.created_at); + } + + async cleanUpUploadTasks(): Promise { + console.log('Cleaning up completed/failed upload tasks...'); + const tasks = this.getUploadTasksFromStorage(); + const filteredTasks = tasks.filter(t => + t.status !== 'success' && t.status !== 'failed' && t.status !== 'skipped' + ); + this.saveUploadTasks(filteredTasks); + } + + async getUploadTasksSince(timestamp: number): Promise { + const tasks = this.getUploadTasksFromStorage(); + const filteredTasks = tasks.filter(t => t.created_at >= timestamp); + return filteredTasks.sort((a, b) => b.created_at - a.created_at); + } + + async exist_pending_tasks(): Promise { + const tasks = this.getUploadTasksFromStorage(); + return tasks.some(t => t.status === 'pending' || t.status === 'uploading'); + } + + async filterExistingFiles(fileUris: string[]): Promise { + if (fileUris.length === 0) { + return []; + } + + const tasks = this.getUploadTasksFromStorage(); + const successfulUris = new Set( + tasks.filter(t => t.status === 'success').map(t => t.uri) + ); + + const newFileUris = fileUris.filter(uri => !successfulUris.has(uri)); + + console.log(`[WebDB] Total files: ${fileUris.length}, Existing successful files: ${successfulUris.size}, New files to upload: ${newFileUris.length}`); + + return newFileUris; + } + + async setAppState(key: string, value: string | null): Promise { + console.log(`Setting app state: ${key} = ${value}`); + const state = this.getAppStateData(); + if (value === null) { + delete state[key]; + } else { + state[key] = value; + } + this.saveAppStateData(state); + } + + async getAppState(key: string): Promise { + const state = this.getAppStateData(); + return state[key] || null; + } + + async executeSql(sql: string, params: any[] = []): Promise { + console.warn('SQL execution not supported in web environment:', sql); + return { error: 'SQL execution not supported in web environment' }; + } +} diff --git a/lib/database/types.ts b/lib/database/types.ts new file mode 100644 index 0000000..848aee6 --- /dev/null +++ b/lib/database/types.ts @@ -0,0 +1,24 @@ +export type UploadTask = { + uri: string; + filename: string; + status: 'pending' | 'uploading' | 'success' | 'failed' | 'skipped'; + progress: number; // 0-100 + file_id?: string; // 后端返回的文件ID + created_at: number; // unix timestamp +}; + +export interface DatabaseInterface { + initUploadTable(): Promise; + insertUploadTask(task: Omit): Promise; + getUploadTaskStatus(uri: string): Promise; + updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise; + updateUploadTaskProgress(uri: string, progress: number): Promise; + getUploadTasks(): Promise; + cleanUpUploadTasks(): Promise; + getUploadTasksSince(timestamp: number): Promise; + exist_pending_tasks(): Promise; + filterExistingFiles(fileUris: string[]): Promise; + setAppState(key: string, value: string | null): Promise; + getAppState(key: string): Promise; + executeSql(sql: string, params?: any[]): Promise; +} diff --git a/lib/database/web-database.ts b/lib/database/web-database.ts new file mode 100644 index 0000000..38d7201 --- /dev/null +++ b/lib/database/web-database.ts @@ -0,0 +1,139 @@ +import { DatabaseInterface, UploadTask } from './types'; + +export class WebDatabase implements DatabaseInterface { + private getStorageKey(table: string): string { + return `memowake_${table}`; + } + + private getUploadTasksFromStorage(): UploadTask[] { + const data = localStorage.getItem(this.getStorageKey('upload_tasks')); + return data ? JSON.parse(data) : []; + } + + private saveUploadTasks(tasks: UploadTask[]): void { + localStorage.setItem(this.getStorageKey('upload_tasks'), JSON.stringify(tasks)); + } + + private getAppStateData(): Record { + const data = localStorage.getItem(this.getStorageKey('app_state')); + return data ? JSON.parse(data) : {}; + } + + private saveAppStateData(state: Record): void { + localStorage.setItem(this.getStorageKey('app_state'), JSON.stringify(state)); + } + + async initUploadTable(): Promise { + console.log('Initializing web storage tables...'); + // Web端不需要初始化表结构,localStorage会自动处理 + } + + async insertUploadTask(task: Omit): Promise { + console.log('Inserting upload task:', task.uri); + const tasks = this.getUploadTasksFromStorage(); + const existingIndex = tasks.findIndex(t => t.uri === task.uri); + const newTask: UploadTask = { + ...task, + created_at: Math.floor(Date.now() / 1000) + }; + + if (existingIndex >= 0) { + tasks[existingIndex] = newTask; + } else { + tasks.push(newTask); + } + + this.saveUploadTasks(tasks); + } + + async getUploadTaskStatus(uri: string): Promise { + console.log('Checking upload task status for:', uri); + const tasks = this.getUploadTasksFromStorage(); + return tasks.find(t => t.uri === uri) || null; + } + + async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise { + const tasks = this.getUploadTasksFromStorage(); + const taskIndex = tasks.findIndex(t => t.uri === uri); + if (taskIndex >= 0) { + tasks[taskIndex].status = status; + if (file_id) { + tasks[taskIndex].file_id = file_id; + } + this.saveUploadTasks(tasks); + } + } + + async updateUploadTaskProgress(uri: string, progress: number): Promise { + const tasks = this.getUploadTasksFromStorage(); + const taskIndex = tasks.findIndex(t => t.uri === uri); + if (taskIndex >= 0) { + tasks[taskIndex].progress = progress; + this.saveUploadTasks(tasks); + } + } + + async getUploadTasks(): Promise { + console.log('Fetching all upload tasks... time:', new Date().toLocaleString()); + const tasks = this.getUploadTasksFromStorage(); + return tasks.sort((a, b) => b.created_at - a.created_at); + } + + async cleanUpUploadTasks(): Promise { + console.log('Cleaning up completed/failed upload tasks...'); + const tasks = this.getUploadTasksFromStorage(); + const filteredTasks = tasks.filter(t => + t.status !== 'success' && t.status !== 'failed' && t.status !== 'skipped' + ); + this.saveUploadTasks(filteredTasks); + } + + async getUploadTasksSince(timestamp: number): Promise { + const tasks = this.getUploadTasksFromStorage(); + const filteredTasks = tasks.filter(t => t.created_at >= timestamp); + return filteredTasks.sort((a, b) => b.created_at - a.created_at); + } + + async exist_pending_tasks(): Promise { + const tasks = this.getUploadTasksFromStorage(); + return tasks.some(t => t.status === 'pending' || t.status === 'uploading'); + } + + async filterExistingFiles(fileUris: string[]): Promise { + if (fileUris.length === 0) { + return []; + } + + const tasks = this.getUploadTasksFromStorage(); + const successfulUris = new Set( + tasks.filter(t => t.status === 'success').map(t => t.uri) + ); + + const newFileUris = fileUris.filter(uri => !successfulUris.has(uri)); + + console.log(`[WebDB] Total files: ${fileUris.length}, Existing successful files: ${successfulUris.size}, New files to upload: ${newFileUris.length}`); + + return newFileUris; + } + + async setAppState(key: string, value: string | null): Promise { + console.log(`Setting app state: ${key} = ${value}`); + const state = this.getAppStateData(); + if (value === null) { + delete state[key]; + } else { + state[key] = value; + } + this.saveAppStateData(state); + } + + async getAppState(key: string): Promise { + const state = this.getAppStateData(); + return state[key] || null; + } + + async executeSql(sql: string, params: any[] = []): Promise { + console.warn('SQL execution not supported in web environment:', sql); + return { error: 'SQL execution not supported in web environment' }; + } +} diff --git a/lib/db.ts b/lib/db.ts index 5aadded..217cb12 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,176 +1,23 @@ -import * as SQLite from 'expo-sqlite'; +// 使用数据库接口架构,支持 Web 和移动端 +import { database } from './database/database-factory'; +import { UploadTask } from './database/types'; -const db = SQLite.openDatabaseSync('upload_status.db'); - -// Set a busy timeout to handle concurrent writes and avoid "database is locked" errors. -// This will make SQLite wait for 5 seconds if the database is locked by another process. -db.execSync('PRAGMA busy_timeout = 5000;'); - -export type UploadTask = { - uri: string; - filename: string; - status: 'pending' | 'uploading' | 'success' | 'failed' | 'skipped'; - progress: number; // 0-100 - file_id?: string; // 后端返回的文件ID - created_at: number; // unix timestamp -}; - -// 初始化表 -export async function initUploadTable() { - console.log('Initializing upload tasks table...'); - await db.execAsync(` - CREATE TABLE IF NOT EXISTS upload_tasks ( - uri TEXT PRIMARY KEY NOT NULL, - filename TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - progress INTEGER NOT NULL DEFAULT 0, - file_id TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) - ); - `); - - // Add created_at column to existing table if it doesn't exist - const columns = await db.getAllAsync('PRAGMA table_info(upload_tasks);'); - const columnExists = columns.some((column: any) => column.name === 'created_at'); - - if (!columnExists) { - console.log('Adding created_at column to upload_tasks table...'); - // SQLite doesn't support non-constant DEFAULT values on ALTER TABLE. - // So we add the column, then update existing rows. - await db.execAsync(`ALTER TABLE upload_tasks ADD COLUMN created_at INTEGER;`); - await db.execAsync(`UPDATE upload_tasks SET created_at = (strftime('%s', 'now')) WHERE created_at IS NULL;`); - console.log('created_at column added and populated.'); - } - console.log('Upload tasks table initialized'); - - await db.execAsync(` - CREATE TABLE IF NOT EXISTS app_state ( - key TEXT PRIMARY KEY NOT NULL, - value TEXT - ); - `); - console.log('App state table initialized'); -} - -// 插入新的上传任务 -export async function insertUploadTask(task: Omit) { - console.log('Inserting upload task:', task.uri); - await db.runAsync( - 'INSERT OR REPLACE INTO upload_tasks (uri, filename, status, progress, file_id, created_at) VALUES (?, ?, ?, ?, ?, ?)', - [task.uri, task.filename, task.status, task.progress, task.file_id ?? null, Math.floor(Date.now() / 1000)] - ); -} - -// 检查文件是否已上传或正在上传 -export async function getUploadTaskStatus(uri: string): Promise { - console.log('Checking upload task status for:', uri); - const result = await db.getFirstAsync( - 'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks WHERE uri = ?;', - uri - ); - return result || null; -} - -// 更新上传任务的状态 -export async function updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string) { - if (file_id) { - await db.runAsync('UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?', [status, file_id, uri]); - } else { - await db.runAsync('UPDATE upload_tasks SET status = ? WHERE uri = ?', [status, uri]); - } -} - -// 更新上传任务的进度 -export async function updateUploadTaskProgress(uri: string, progress: number) { - await db.runAsync('UPDATE upload_tasks SET progress = ? WHERE uri = ?', [progress, uri]); -} - -// 获取所有上传任务 -export async function getUploadTasks(): Promise { - console.log('Fetching all upload tasks... time:', new Date().toLocaleString()); - const results = await db.getAllAsync( - 'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks ORDER BY created_at DESC;' - ); - return results; -} - -// 清理已完成或失败的任务 (可选,根据需求添加) -export async function cleanUpUploadTasks(): Promise { - console.log('Cleaning up completed/failed upload tasks...'); - await db.runAsync( - "DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';" - ); -} - -// 获取某个时间点之后的所有上传任务 -export async function getUploadTasksSince(timestamp: number): Promise { - const rows = await db.getAllAsync( - 'SELECT * FROM upload_tasks WHERE created_at >= ? ORDER BY created_at DESC', - [timestamp] - ); - return rows; -} - -export async function exist_pending_tasks(): Promise { - const rows = await db.getAllAsync( - 'SELECT * FROM upload_tasks WHERE status = "pending" OR status = "uploading"' - ); - return rows.length > 0; -} - -// 检查一组文件URI,返回那些在数据库中不存在或是未成功上传的文件的URI -export async function filterExistingFiles(fileUris: string[]): Promise { - if (fileUris.length === 0) { - return []; - } - - // 创建占位符字符串 '?,?,?' - const placeholders = fileUris.map(() => '?').join(','); - - // 查询已经存在且状态为 'success' 的任务 - const query = `SELECT uri FROM upload_tasks WHERE uri IN (${placeholders}) AND status = 'success'`; - - const existingFiles = await db.getAllAsync<{ uri: string }>(query, fileUris); - const existingUris = new Set(existingFiles.map(f => f.uri)); - - // 过滤出新文件 - const newFileUris = fileUris.filter(uri => !existingUris.has(uri)); - - console.log(`[DB] Total files: ${fileUris.length}, Existing successful files: ${existingUris.size}, New files to upload: ${newFileUris.length}`); - - return newFileUris; -} - -// 设置全局状态值 -export async function setAppState(key: string, value: string | null): Promise { - console.log(`Setting app state: ${key} = ${value}`); - await db.runAsync('INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)', [key, value]); -} - -// 获取全局状态值 -export async function getAppState(key: string): Promise { - const result = await db.getFirstAsync<{ value: string }>('SELECT value FROM app_state WHERE key = ?;', key); - return result?.value ?? null; -} - -// for debug page -export async function executeSql(sql: string, params: any[] = []): Promise { - try { - // Trim and check if it's a SELECT query - const isSelect = sql.trim().toLowerCase().startsWith('select'); - if (isSelect) { - const results = db.getAllSync(sql, params); - return results; - } else { - const result = db.runSync(sql, params); - return { - changes: result.changes, - lastInsertRowId: result.lastInsertRowId, - }; - } - } catch (error: any) { - console.error("Error executing SQL:", error); - return { error: error.message }; - } -} +// 重新导出类型 +export type { UploadTask }; +// 重新导出所有数据库函数,使用统一接口 +export const initUploadTable = () => database.initUploadTable(); +export const insertUploadTask = (task: Omit) => database.insertUploadTask(task); +export const getUploadTaskStatus = (uri: string) => database.getUploadTaskStatus(uri); +export const updateUploadTaskStatus = (uri: string, status: UploadTask['status'], file_id?: string) => + database.updateUploadTaskStatus(uri, status, file_id); +export const updateUploadTaskProgress = (uri: string, progress: number) => + database.updateUploadTaskProgress(uri, progress); +export const getUploadTasks = () => database.getUploadTasks(); +export const cleanUpUploadTasks = () => database.cleanUpUploadTasks(); +export const getUploadTasksSince = (timestamp: number) => database.getUploadTasksSince(timestamp); +export const exist_pending_tasks = () => database.exist_pending_tasks(); +export const filterExistingFiles = (fileUris: string[]) => database.filterExistingFiles(fileUris); +export const setAppState = (key: string, value: string | null) => database.setAppState(key, value); +export const getAppState = (key: string) => database.getAppState(key); +export const executeSql = (sql: string, params: any[] = []) => database.executeSql(sql, params); diff --git a/metro.config.js b/metro.config.js index 7ba56c4..a20435c 100644 --- a/metro.config.js +++ b/metro.config.js @@ -19,6 +19,15 @@ config.resolver = { ...config.resolver?.alias, '@/': path.resolve(__dirname, './'), }, + platforms: ['ios', 'android', 'native', 'web'], }; +// Web 环境下的模块别名 +if (process.env.EXPO_PLATFORM === 'web') { + config.resolver.alias = { + ...config.resolver.alias, + 'expo-sqlite': path.resolve(__dirname, './lib/database/empty-sqlite.js'), + }; +} + module.exports = withNativeWind(config, { input: './global.css' }); \ No newline at end of file