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
+71 -1
View File
@@ -110,7 +110,7 @@ export async function getUserEmail(accessToken: string): Promise<string> {
// drive.file scope'u sadece uygulamanın oluşturduğu/açtığı dosyaları görmesine
// izin verir — bu yüzden klasörü her zaman biz oluşturuyoruz/arıyoruz, kullanıcının
// genel Drive'ında serbest arama yapamayız (zaten istemiyoruz, bkz. PRD §7).
async function findOrCreateFolder(accessToken: string, name: string, parentId?: string): Promise<string> {
export async function findOrCreateFolder(accessToken: string, name: string, parentId?: string): Promise<string> {
const conditions = [`name='${name.replace(/'/g, "\\'")}'`, "mimeType='application/vnd.google-apps.folder'", 'trashed=false'];
if (parentId) conditions.push(`'${parentId}' in parents`);
@@ -146,3 +146,73 @@ export async function ensureAyrisLegalFolders(accessToken: string): Promise<{ ro
const casesFolderId = await findOrCreateFolder(accessToken, 'Davalar', rootFolderId);
return { rootFolderId, casesFolderId };
}
// PRD §9: local dosya yapısı (dava klasörü → belgeler) birebir Drive'a
// yansıtılıyor — dava başlığı = klasör adı. drive.file scope zaten sadece
// uygulamanın oluşturduğu klasörleri görebildiği için burada da arama/oluşturma
// findOrCreateFolder ile aynı mantıkla çalışıyor.
export async function findOrCreateCaseFolder(accessToken: string, casesFolderId: string, caseTitle: string): Promise<string> {
return findOrCreateFolder(accessToken, caseTitle, casesFolderId);
}
interface UploadParams {
name: string;
parentId: string;
mimeType: string;
buffer: Buffer;
// Verilirse içerik bu mevcut Drive dosyasının YENİ SÜRÜMÜ olarak yüklenir
// (Google Drive eski sürümü otomatik revizyon geçmişinde tutar) — yeni bir
// kopya dosya OLUŞTURULMAZ. PRD Test 4 "Modified File" tam olarak bu.
existingFileId?: string;
}
// PRD §16: Google Drive'ın resumable upload protokolü kullanılıyor (electron-updater
// gibi burada da sıfırdan bir upload mekanizması yazmak yerine Google'ın kendi
// resumable session'ı kullanılıyor). Dosya baytları backend'e tek seferde (multer,
// bellekte buffer olarak) geldiği için PUT tek parça gönderiliyor — asıl fayda
// backend↔Google bacağının bu sayede Google'ın kendi bütünlük/aktarım garantisini
// kullanması. Not: Electron↔backend bacağı için bayt-bazlı devam etme (resume)
// henüz yok, bkz. PR açıklaması — kuyruk seviyesinde tüm dosya yeniden denenir.
export async function uploadFileResumable(accessToken: string, params: UploadParams): Promise<{ id: string; modifiedTime?: string }> {
const isUpdate = !!params.existingFileId;
const sessionUrl = isUpdate
? `https://www.googleapis.com/upload/drive/v3/files/${params.existingFileId}?uploadType=resumable`
: `https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable`;
const metadata = isUpdate ? {} : { name: params.name, parents: [params.parentId] };
const sessionRes = await fetch(sessionUrl, {
method: isUpdate ? 'PATCH' : 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json; charset=UTF-8',
'X-Upload-Content-Type': params.mimeType,
'X-Upload-Content-Length': String(params.buffer.length),
},
body: JSON.stringify(metadata),
});
if (!sessionRes.ok) {
const err = await sessionRes.json().catch(() => ({}));
throw new Error(err.error?.message || 'Google Drive upload oturumu başlatılamadı.');
}
const sessionUri = sessionRes.headers.get('location');
if (!sessionUri) {
throw new Error('Google Drive upload oturum adresi alınamadı.');
}
const uploadRes = await fetch(sessionUri, {
method: 'PUT',
headers: {
'Content-Type': params.mimeType,
'Content-Length': String(params.buffer.length),
},
// Node'un fetch implementasyonu (undici) Buffer'ı runtime'da sorunsuz kabul
// ediyor — DOM tip tanımları BodyInit'e Buffer'ı dahil etmediği için sadece
// tip seviyesinde cast gerekiyor.
body: params.buffer as unknown as BodyInit,
});
const data = await uploadRes.json().catch(() => ({}));
if (!uploadRes.ok) {
throw new Error(data.error?.message || `Google Drive upload başarısız (HTTP ${uploadRes.status}).`);
}
return data;
}