From ce6fa9deaebf8217081c7447e6c63dcb41708e00 Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Mon, 10 Aug 2026 04:24:56 +0300 Subject: [PATCH] feat: Google Drive restore endpoints (Faz 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/google-drive/backup-files: kullanıcının senkronize edilmiş dosyalarının listesi (PRD §28 "N dosya bulundu"). GET /download: bir dosyayı Drive'dan proxy'leyip ham baytları döner — istenen drive_file_id'nin gerçekten bu kullanıcıya ait olduğu önce doğrulanıyor (PRD §39, başka kullanıcının backup'ı sızmasın). Co-Authored-By: Claude Sonnet 5 --- src/controllers/googleDrive.controller.ts | 59 +++++++++++++++++++++++ src/lib/googleDriveClient.ts | 15 ++++++ src/routes/googleDrive.routes.ts | 4 +- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/controllers/googleDrive.controller.ts b/src/controllers/googleDrive.controller.ts index e2fece1..d70aef7 100644 --- a/src/controllers/googleDrive.controller.ts +++ b/src/controllers/googleDrive.controller.ts @@ -322,3 +322,62 @@ export const getBackupStatus = async (req: AuthenticatedRequest, res: Response) res.status(500).json({ error: 'Yedekleme durumu alınamadı.' }); } }; + +// Faz 3 — Restore. PRD §28: "Google Drive Backup — N dosya bulundu [Geri Yükle]". +// Electron'un restoreQueue.js'i bu listeyi çekip yerelde hangi dosyaların eksik +// olduğuna kendisi karar veriyor (backend local diski bilmiyor). +export const listBackupFiles = async (req: AuthenticatedRequest, res: Response) => { + try { + const userId = req.user?.id; + const { data, error } = await supabase + .from('google_drive_files') + .select('local_path, file_name, drive_file_id, file_size, drive_modified_at') + .eq('user_id', userId) + .eq('status', 'synced') + .order('local_path', { ascending: true }); + if (error) throw error; + res.json({ files: data || [] }); + } catch (error: any) { + console.error('[DRIVE] List Backup Files Error:', error); + res.status(500).json({ error: 'Yedeklenen dosya listesi alınamadı.' }); + } +}; + +// Tek bir dosyayı Drive'dan indirip ham baytları döner. PRD §39: kullanıcı +// başka bir kullanıcının backup'ını indiremesin — bu yüzden istenen +// drive_file_id'nin gerçekten bu kullanıcının google_drive_files kaydında +// olduğu ÖNCE doğrulanıyor (aksi halde herhangi bir Drive dosya ID'si verilip +// backend'in geçerli token'ı üzerinden başkasının verisi sızdırılabilirdi). +export const downloadFile = async (req: AuthenticatedRequest, res: Response) => { + try { + const userId = req.user?.id as string; + const driveFileId = String(req.query.drive_file_id || ''); + if (!driveFileId) { + return res.status(400).json({ error: 'drive_file_id zorunludur.' }); + } + + const { data: fileRow } = await supabase + .from('google_drive_files') + .select('id, file_name') + .eq('user_id', userId) + .eq('drive_file_id', driveFileId) + .maybeSingle(); + if (!fileRow) { + return res.status(403).json({ error: 'Bu dosyaya erişim yetkiniz yok.' }); + } + + let accessToken: string; + try { + accessToken = await getValidAccessToken(userId); + } catch (err: any) { + return res.status(409).json({ error: err.message || 'Google Drive bağlantınızın yenilenmesi gerekiyor.' }); + } + + const buffer = await googleDrive.downloadFile(accessToken, driveFileId); + res.setHeader('Content-Type', 'application/octet-stream'); + res.send(buffer); + } catch (error: any) { + console.error('[DRIVE] Download Error:', error); + res.status(500).json({ error: 'Dosya indirilemedi.' }); + } +}; diff --git a/src/lib/googleDriveClient.ts b/src/lib/googleDriveClient.ts index f9a5988..f6845d3 100644 --- a/src/lib/googleDriveClient.ts +++ b/src/lib/googleDriveClient.ts @@ -216,3 +216,18 @@ export async function uploadFileResumable(accessToken: string, params: UploadPar } return data; } + +// Faz 3 — Restore: bir dosyanın ham baytlarını Drive'dan indirir. Electron'un +// Google credential'ı hiç olmadığı için (upload'ta olduğu gibi) bu da backend +// üzerinden proxy'leniyor — çağıran taraf (googleDrive.controller.ts) fileId'nin +// gerçekten bu kullanıcıya ait olduğunu ÖNCE doğrulamalı. +export async function downloadFile(accessToken: string, fileId: string): Promise { + const res = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!res.ok) { + throw new Error(`Google Drive dosyası indirilemedi (HTTP ${res.status}).`); + } + const arrayBuffer = await res.arrayBuffer(); + return Buffer.from(arrayBuffer); +} diff --git a/src/routes/googleDrive.routes.ts b/src/routes/googleDrive.routes.ts index e28dc29..dc4f2e7 100644 --- a/src/routes/googleDrive.routes.ts +++ b/src/routes/googleDrive.routes.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import multer from 'multer'; import { requireAuth } from '../middleware/auth'; -import { connect, oauthCallback, getStatus, disconnect, refresh, uploadFile, getBackupStatus } from '../controllers/googleDrive.controller'; +import { connect, oauthCallback, getStatus, disconnect, refresh, uploadFile, getBackupStatus, listBackupFiles, downloadFile } from '../controllers/googleDrive.controller'; const router = Router(); const upload = multer({ storage: multer.memoryStorage() }); @@ -15,5 +15,7 @@ router.post('/disconnect', requireAuth, disconnect); router.post('/refresh', requireAuth, refresh); router.post('/upload', upload.single('file'), requireAuth, uploadFile); router.get('/backup-status', requireAuth, getBackupStatus); +router.get('/backup-files', requireAuth, listBackupFiles); +router.get('/download', requireAuth, downloadFile); export default router;