feat: Google Drive backup upload endpoint (Faz 2 — backend half)

POST /api/google-drive/upload: Electron'un backupQueue'sunun
gönderdiği tek dosyayı alıp Google'ın resumable upload session'ıyla
Drive'a yazıyor (mevcutsa aynı drive_file_id'yi PATCH ederek yeni
sürüm olarak, PRD Test 4). Dava başlığı = Drive'daki alt klasör adı
(lokal yapı korunuyor). GET /backup-status: Ayarlar > Yedekleme
paneli için toplam dosya/boyut/son senkron özeti.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mstfyldz
2026-08-10 04:14:27 +03:00
co-authored by Claude Sonnet 5
parent 328aef91bd
commit 313d5b72ef
3 changed files with 213 additions and 2 deletions
+137
View File
@@ -185,3 +185,140 @@ export const refresh = async (req: AuthenticatedRequest, res: Response) => {
res.status(409).json({ error: error.message || 'Google Drive bağlantısı yenilenemedi.' });
}
};
// Electron'un backupQueue.js'i tek bir dosyayı burada yükler — Google Drive
// çağrısı (resumable upload) tamamen burada yapılıyor, Electron hiçbir zaman
// Google token'ı görmüyor. PRD §9: local yapı korunuyor, dava başlığı = Drive
// klasör adı (Davalar/<dava başlığı>/<dosya>).
export const uploadFile = async (req: AuthenticatedRequest, res: Response) => {
try {
const userId = req.user?.id as string;
const file = req.file;
const { case_title, local_path, local_modified_at, sha256 } = req.body || {};
if (!file || !case_title || !local_path) {
return res.status(400).json({ error: 'file, case_title ve local_path zorunludur.' });
}
const { data: connection } = await supabase
.from('google_drive_connections')
.select('drive_cases_folder_id, status')
.eq('user_id', userId)
.maybeSingle();
if (!connection || connection.status !== 'connected' || !connection.drive_cases_folder_id) {
return res.status(409).json({ error: 'Google Drive bağlantısı bulunamadı.' });
}
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 caseFolderId = await googleDrive.findOrCreateCaseFolder(accessToken, connection.drive_cases_folder_id, case_title);
const { data: existingFile } = await supabase
.from('google_drive_files')
.select('id, drive_file_id')
.eq('user_id', userId)
.eq('local_path', local_path)
.maybeSingle();
console.log('[DRIVE] Upload started:', local_path);
await supabase.from('google_drive_backup_events').insert({
user_id: userId,
file_id: existingFile?.id || null,
event_type: 'upload_started',
file_size: file.size,
});
let driveResult;
try {
driveResult = await googleDrive.uploadFileResumable(accessToken, {
name: file.originalname,
parentId: caseFolderId,
mimeType: file.mimetype || 'application/octet-stream',
buffer: file.buffer,
existingFileId: existingFile?.drive_file_id || undefined,
});
} catch (uploadErr: any) {
console.error('[DRIVE] Upload failed:', local_path, uploadErr.message);
await supabase.from('google_drive_backup_events').insert({
user_id: userId,
file_id: existingFile?.id || null,
event_type: 'upload_failed',
file_size: file.size,
error_message: uploadErr.message,
});
throw uploadErr;
}
const { data: fileRow, error: upsertError } = await supabase
.from('google_drive_files')
.upsert(
{
user_id: userId,
local_path,
drive_file_id: driveResult.id,
drive_parent_id: caseFolderId,
file_name: file.originalname,
file_size: file.size,
local_modified_at: local_modified_at || null,
drive_modified_at: new Date().toISOString(),
sha256: sha256 || null,
status: 'synced',
updated_at: new Date().toISOString(),
},
{ onConflict: 'user_id,local_path' }
)
.select('id')
.single();
if (upsertError) throw upsertError;
await supabase.from('google_drive_backup_events').insert({
user_id: userId,
file_id: fileRow.id,
event_type: 'upload_completed',
file_size: file.size,
status: 'synced',
});
await supabase.from('google_drive_connections').update({ last_sync_at: new Date().toISOString() }).eq('user_id', userId);
console.log('[DRIVE] Upload completed:', local_path);
res.json({ ok: true, drive_file_id: driveResult.id });
} catch (error: any) {
console.error('[DRIVE] Upload Error:', error);
// PRD §41: teknik detay kullanıcıya gitmiyor, sadece backend logunda kalıyor.
res.status(500).json({ error: 'Dosya yedeklenemedi. İnternet bağlantınızı kontrol edin.' });
}
};
export const getBackupStatus = async (req: AuthenticatedRequest, res: Response) => {
try {
const userId = req.user?.id;
const { data: files, error } = await supabase
.from('google_drive_files')
.select('status, file_size')
.eq('user_id', userId);
if (error) throw error;
const { data: connection } = await supabase
.from('google_drive_connections')
.select('last_sync_at')
.eq('user_id', userId)
.maybeSingle();
const synced = (files || []).filter((f) => f.status === 'synced');
const totalSize = synced.reduce((sum, f) => sum + (f.file_size || 0), 0);
res.json({
total_files: synced.length,
total_size: totalSize,
last_sync_at: connection?.last_sync_at || null,
});
} catch (error: any) {
console.error('[DRIVE] Get Backup Status Error:', error);
res.status(500).json({ error: 'Yedekleme durumu alınamadı.' });
}
};