From 9edaa647c4ef7f8634669d0c985c62590f958a29 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 19 Feb 2026 02:27:56 +0800 Subject: [PATCH] feat(storage): add method to retrieve attachments by user ID for improved data handling --- src/handlers/ciphers.ts | 2 +- src/handlers/import.ts | 60 +++++++++++++++++++++++++++++++++++++++-- src/handlers/sync.ts | 2 +- src/services/storage.ts | 32 ++++++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 28ef9fb..dd4b8b9 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -77,7 +77,7 @@ export async function handleGetCiphers(request: Request, env: Env, userId: strin : ciphers.filter(c => !c.deletedAt); } - const attachmentsByCipher = await storage.getAttachmentsByCipherIds(filteredCiphers.map(c => c.id)); + const attachmentsByCipher = await storage.getAttachmentsByUserId(userId); // Get attachments for all ciphers const cipherResponses = []; diff --git a/src/handlers/import.ts b/src/handlers/import.ts index 106cd48..4fde9ac 100644 --- a/src/handlers/import.ts +++ b/src/handlers/import.ts @@ -2,6 +2,7 @@ import { Env, Cipher, Folder, CipherType } from '../types'; import { StorageService } from '../services/storage'; import { errorResponse } from '../utils/response'; import { generateUUID } from '../utils/uuid'; +import { LIMITS } from '../config/limits'; // Bitwarden client import request format interface CiphersImportRequest { @@ -66,6 +67,17 @@ interface CiphersImportRequest { }>; } +function bindNull(v: any): any { + return v === undefined ? null : v; +} + +async function runBatchInChunks(db: D1Database, statements: D1PreparedStatement[], chunkSize: number): Promise { + for (let i = 0; i < statements.length; i += chunkSize) { + const chunk = statements.slice(i, i + chunkSize); + await db.batch(chunk); + } +} + // POST /api/ciphers/import - Bitwarden client import endpoint export async function handleCiphersImport(request: Request, env: Env, userId: string): Promise { const storage = new StorageService(env.DB); @@ -82,9 +94,11 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st const folderRelationships = importData.folderRelationships || []; const now = new Date().toISOString(); + const batchChunkSize = LIMITS.performance.bulkMoveChunkSize; // Create folders and build index -> id mapping const folderIdMap = new Map(); + const folderRows: Folder[] = []; for (let i = 0; i < folders.length; i++) { const folderId = generateUUID(); @@ -98,7 +112,19 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st updatedAt: now, }; - await storage.saveFolder(folder); + folderRows.push(folder); + } + + if (folderRows.length > 0) { + const folderStatements = folderRows.map(folder => + env.DB + .prepare( + 'INSERT INTO folders(id, user_id, name, created_at, updated_at) VALUES(?, ?, ?, ?, ?) ' + + 'ON CONFLICT(id) DO UPDATE SET user_id=excluded.user_id, name=excluded.name, updated_at=excluded.updated_at' + ) + .bind(folder.id, folder.userId, folder.name, folder.createdAt, folder.updatedAt) + ); + await runBatchInChunks(env.DB, folderStatements, batchChunkSize); } // Build cipher index -> folder id mapping from relationships @@ -111,6 +137,7 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st } // Create ciphers + const cipherRows: Cipher[] = []; for (let i = 0; i < ciphers.length; i++) { const c = ciphers[i]; const folderId = cipherFolderMap.get(i) || null; @@ -181,7 +208,36 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st deletedAt: null, }; - await storage.saveCipher(cipher); + cipherRows.push(cipher); + } + + if (cipherRows.length > 0) { + const cipherStatements = cipherRows.map(cipher => { + const data = JSON.stringify(cipher); + return env.DB + .prepare( + 'INSERT INTO ciphers(id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, deleted_at) ' + + 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' + + 'ON CONFLICT(id) DO UPDATE SET ' + + 'user_id=excluded.user_id, type=excluded.type, folder_id=excluded.folder_id, name=excluded.name, notes=excluded.notes, favorite=excluded.favorite, data=excluded.data, reprompt=excluded.reprompt, key=excluded.key, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at' + ) + .bind( + cipher.id, + cipher.userId, + Number(cipher.type) || 1, + bindNull(cipher.folderId), + bindNull(cipher.name), + bindNull(cipher.notes), + cipher.favorite ? 1 : 0, + data, + bindNull(cipher.reprompt ?? 0), + bindNull(cipher.key), + cipher.createdAt, + cipher.updatedAt, + bindNull(cipher.deletedAt) + ); + }); + await runBatchInChunks(env.DB, cipherStatements, batchChunkSize); } // Update revision date diff --git a/src/handlers/sync.ts b/src/handlers/sync.ts index ee24bba..0c4bb7b 100644 --- a/src/handlers/sync.ts +++ b/src/handlers/sync.ts @@ -60,7 +60,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr const ciphers = await storage.getAllCiphers(userId); const folders = await storage.getAllFolders(userId); - const attachmentsByCipher = await storage.getAttachmentsByCipherIds(ciphers.map(c => c.id)); + const attachmentsByCipher = await storage.getAttachmentsByUserId(userId); // Build profile response const profile: ProfileResponse = { diff --git a/src/services/storage.ts b/src/services/storage.ts index cd5b0ce..1aae936 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -503,6 +503,38 @@ export class StorageService { return grouped; } + async getAttachmentsByUserId(userId: string): Promise> { + const grouped = new Map(); + const res = await this.db + .prepare( + `SELECT a.id, a.cipher_id, a.file_name, a.size, a.size_name, a.key + FROM attachments a + INNER JOIN ciphers c ON c.id = a.cipher_id + WHERE c.user_id = ?` + ) + .bind(userId) + .all(); + + for (const row of (res.results || [])) { + const item: Attachment = { + id: row.id, + cipherId: row.cipher_id, + fileName: row.file_name, + size: row.size, + sizeName: row.size_name, + key: row.key, + }; + const list = grouped.get(item.cipherId); + if (list) { + list.push(item); + } else { + grouped.set(item.cipherId, [item]); + } + } + + return grouped; + } + async addAttachmentToCipher(cipherId: string, attachmentId: string): Promise { // Kept for API compatibility; no-op because attachments table already links cipher_id. // We still validate that the attachment exists and belongs to cipher.