feat: Google Drive restore endpoints (Faz 3)
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
313d5b72ef
commit
ce6fa9deae
@@ -322,3 +322,62 @@ export const getBackupStatus = async (req: AuthenticatedRequest, res: Response)
|
|||||||
res.status(500).json({ error: 'Yedekleme durumu alınamadı.' });
|
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.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -216,3 +216,18 @@ export async function uploadFileResumable(accessToken: string, params: UploadPar
|
|||||||
}
|
}
|
||||||
return data;
|
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<Buffer> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { requireAuth } from '../middleware/auth';
|
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 router = Router();
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
@@ -15,5 +15,7 @@ router.post('/disconnect', requireAuth, disconnect);
|
|||||||
router.post('/refresh', requireAuth, refresh);
|
router.post('/refresh', requireAuth, refresh);
|
||||||
router.post('/upload', upload.single('file'), requireAuth, uploadFile);
|
router.post('/upload', upload.single('file'), requireAuth, uploadFile);
|
||||||
router.get('/backup-status', requireAuth, getBackupStatus);
|
router.get('/backup-status', requireAuth, getBackupStatus);
|
||||||
|
router.get('/backup-files', requireAuth, listBackupFiles);
|
||||||
|
router.get('/download', requireAuth, downloadFile);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user