feat(storage): add method to retrieve attachments by user ID for improved data handling

This commit is contained in:
shuaiplus
2026-02-19 02:27:56 +08:00
parent 081dc64093
commit f63b5d6cf4
4 changed files with 92 additions and 4 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ export async function handleGetCiphers(request: Request, env: Env, userId: strin
: ciphers.filter(c => !c.deletedAt); : 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 // Get attachments for all ciphers
const cipherResponses = []; const cipherResponses = [];
+58 -2
View File
@@ -2,6 +2,7 @@ import { Env, Cipher, Folder, CipherType } from '../types';
import { StorageService } from '../services/storage'; import { StorageService } from '../services/storage';
import { errorResponse } from '../utils/response'; import { errorResponse } from '../utils/response';
import { generateUUID } from '../utils/uuid'; import { generateUUID } from '../utils/uuid';
import { LIMITS } from '../config/limits';
// Bitwarden client import request format // Bitwarden client import request format
interface CiphersImportRequest { 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<void> {
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 // POST /api/ciphers/import - Bitwarden client import endpoint
export async function handleCiphersImport(request: Request, env: Env, userId: string): Promise<Response> { export async function handleCiphersImport(request: Request, env: Env, userId: string): Promise<Response> {
const storage = new StorageService(env.DB); 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 folderRelationships = importData.folderRelationships || [];
const now = new Date().toISOString(); const now = new Date().toISOString();
const batchChunkSize = LIMITS.performance.bulkMoveChunkSize;
// Create folders and build index -> id mapping // Create folders and build index -> id mapping
const folderIdMap = new Map<number, string>(); const folderIdMap = new Map<number, string>();
const folderRows: Folder[] = [];
for (let i = 0; i < folders.length; i++) { for (let i = 0; i < folders.length; i++) {
const folderId = generateUUID(); const folderId = generateUUID();
@@ -98,7 +112,19 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
updatedAt: now, 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 // Build cipher index -> folder id mapping from relationships
@@ -111,6 +137,7 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
} }
// Create ciphers // Create ciphers
const cipherRows: Cipher[] = [];
for (let i = 0; i < ciphers.length; i++) { for (let i = 0; i < ciphers.length; i++) {
const c = ciphers[i]; const c = ciphers[i];
const folderId = cipherFolderMap.get(i) || null; const folderId = cipherFolderMap.get(i) || null;
@@ -181,7 +208,36 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
deletedAt: null, 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 // Update revision date
+1 -1
View File
@@ -60,7 +60,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
const ciphers = await storage.getAllCiphers(userId); const ciphers = await storage.getAllCiphers(userId);
const folders = await storage.getAllFolders(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 // Build profile response
const profile: ProfileResponse = { const profile: ProfileResponse = {
+32
View File
@@ -503,6 +503,38 @@ export class StorageService {
return grouped; return grouped;
} }
async getAttachmentsByUserId(userId: string): Promise<Map<string, Attachment[]>> {
const grouped = new Map<string, Attachment[]>();
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<any>();
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<void> { async addAttachmentToCipher(cipherId: string, attachmentId: string): Promise<void> {
// Kept for API compatibility; no-op because attachments table already links cipher_id. // Kept for API compatibility; no-op because attachments table already links cipher_id.
// We still validate that the attachment exists and belongs to cipher. // We still validate that the attachment exists and belongs to cipher.