From 8625a0fd8484b773c548d687ffb5b3d2ab813a4e Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Sat, 6 Jun 2026 00:04:12 +0300 Subject: [PATCH] feat: update backup features and cron jobs --- src/app/api/backups/test/route.ts | 2 +- src/app/dashboard/databases/page.tsx | 16 +++---- src/lib/backup.ts | 67 ++++++---------------------- src/lib/cronWorker.ts | 2 +- 4 files changed, 23 insertions(+), 64 deletions(-) diff --git a/src/app/api/backups/test/route.ts b/src/app/api/backups/test/route.ts index 5227b8a..90d00d0 100644 --- a/src/app/api/backups/test/route.ts +++ b/src/app/api/backups/test/route.ts @@ -22,7 +22,7 @@ export async function POST(req: NextRequest) { const db = config.databases.find(d => d.id === db_id) if (!db) return NextResponse.json({ ok: false, error: 'Veritabanı bulunamadı.' }) - const filename = backupFilename() + const filename = backupFilename(db.name) try { // 1. Dump al diff --git a/src/app/dashboard/databases/page.tsx b/src/app/dashboard/databases/page.tsx index 21be9fe..dd696b2 100644 --- a/src/app/dashboard/databases/page.tsx +++ b/src/app/dashboard/databases/page.tsx @@ -559,16 +559,16 @@ function BackupTab({ db }: { db: Db }) {
Google Drive Kurulumu
{[ - { n: 1, text: 'console.cloud.google.com → Yeni proje oluştur (veya mevcut)' }, + { n: 1, text: 'console.cloud.google.com → Proje seç/oluştur' }, { n: 2, text: '"APIs & Services" → "Enable APIs" → "Google Drive API" aç' }, - { n: 3, text: '"Credentials" → "Service Accounts" → Yeni hesap oluştur' }, - { n: 4, text: 'Hesaba tıkla → "Keys" → "Add Key" → "JSON" — dosyayı indir' }, - { n: 5, text: 'Google Drive\'da yeni klasör aç → Sağ tık → Paylaş' }, - { n: 6, text: 'Aşağıdaki e-postayı "Düzenleyici" olarak ekle ve klasör ID\'sini kopyala' }, + { n: 3, text: '"Credentials" → "Service Accounts" → Yeni hesap oluştur → JSON key indir' }, + { n: 4, text: 'Google Drive\'da bir klasör oluştur (örn. "VPS Backups")' }, + { n: 5, text: 'Klasörü sağ tıkla → "Paylaş" → aşağıdaki service account e-postasını "Düzenleyici" olarak ekle', highlight: true }, + { n: 6, text: 'Klasörü aç → URL\'deki /folders/XXXXX kısmını Klasör ID alanına yapıştır', highlight: true }, ].map(s => ( -
- {s.n} - {s.text} +
+ {s.n} + {s.text}
))}
diff --git a/src/lib/backup.ts b/src/lib/backup.ts index dd66689..a57b250 100644 --- a/src/lib/backup.ts +++ b/src/lib/backup.ts @@ -14,10 +14,11 @@ export interface Database { db_type?: 'postgres' | 'mysql' } -export function backupFilename(): string { +export function backupFilename(dbName?: string): string { const now = new Date() const pad = (n: number) => String(n).padStart(2, '0') - return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}.sql` + const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}` + return dbName ? `${dbName}_${ts}.sql` : `${ts}.sql` } export async function createDbDumpBuffer(db: Database): Promise { @@ -122,49 +123,6 @@ async function getGoogleAccessToken(credentials: Record): Promis return access_token } -async function getOrCreateDriveFolder( - accessToken: string, - folderName: string, - parentId?: string -): Promise { - // Aynı isimli klasör var mı kontrol et - const parentQuery = parentId ? ` and '${parentId}' in parents` : '' - const q = `name='${folderName.replace(/'/g, "\\'")}' and mimeType='application/vnd.google-apps.folder' and trashed=false${parentQuery}` - - const searchRes = await fetch( - `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=files(id,name)`, - { headers: { Authorization: `Bearer ${accessToken}` } } - ) - - if (searchRes.ok) { - const { files } = await searchRes.json() - if (files?.length > 0) return files[0].id - } - - // Yoksa oluştur - const meta: Record = { - name: folderName, - mimeType: 'application/vnd.google-apps.folder', - } - if (parentId) meta.parents = [parentId] - - const createRes = await fetch('https://www.googleapis.com/drive/v3/files', { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(meta), - }) - - if (!createRes.ok) { - throw new Error(`Klasör oluşturulamadı: ${await createRes.text()}`) - } - - const folder = await createRes.json() - return folder.id -} - export async function uploadToGoogleDrive( serviceAccountJsonStr: string, dbName: string, @@ -172,15 +130,16 @@ export async function uploadToGoogleDrive( content: Buffer, parentFolderId?: string ) { + if (!parentFolderId) { + throw new Error('Klasör ID zorunlu. Drive\'da bir klasör oluşturun, service account ile paylaşın ve ID\'sini girin.') + } + const credentials = JSON.parse(serviceAccountJsonStr) const accessToken = await getGoogleAccessToken(credentials) - // DB adında alt klasör bul veya oluştur - const targetFolderId = await getOrCreateDriveFolder(accessToken, dbName, parentFolderId || undefined) - - // Multipart upload const boundary = 'vps_panel_backup_boundary' - const meta = JSON.stringify({ name: filename, parents: [targetFolderId] }) + // parents açıkça belirtilmezse service account kendi kotasız alanına yüklemeye çalışır + const meta = JSON.stringify({ name: filename, parents: [parentFolderId] }) const parts = Buffer.concat([ Buffer.from(`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${meta}\r\n`), @@ -190,7 +149,7 @@ export async function uploadToGoogleDrive( ]) const response = await fetch( - 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name', + 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name&supportsAllDrives=true', { method: 'POST', headers: { @@ -224,17 +183,17 @@ export async function testGoogleDriveCredentials( if (folderId) { const folderRes = await fetch( - `https://www.googleapis.com/drive/v3/files/${folderId}?fields=id,name`, + `https://www.googleapis.com/drive/v3/files/${folderId}?fields=id,name&supportsAllDrives=true`, { headers: { Authorization: `Bearer ${accessToken}` } } ) if (!folderRes.ok) { return { ok: false, - error: `Token alındı ama klasöre erişilemiyor. "${credentials.client_email}" adresini klasörde Düzenleyici olarak eklediniz mi?`, + error: `Token alındı ama Drive'a erişilemiyor. Service account'u Paylaşımlı Drive'a üye olarak eklediniz mi? (${credentials.client_email})`, } } const folder = await folderRes.json() - return { ok: true, detail: `Bağlantı OK — Klasör: "${folder.name}" — Hesap: ${credentials.client_email}` } + return { ok: true, detail: `Bağlantı OK — Drive: "${folder.name}" — Hesap: ${credentials.client_email}` } } return { ok: true, detail: `Token alındı — Hesap: ${credentials.client_email}` } diff --git a/src/lib/cronWorker.ts b/src/lib/cronWorker.ts index b59b857..8b1c5d7 100644 --- a/src/lib/cronWorker.ts +++ b/src/lib/cronWorker.ts @@ -28,7 +28,7 @@ export async function reloadBackupCrons() { console.log(`[Backup] ${db.name} (${row.cloud_type}) başlatılıyor...`) const buffer = await createDbDumpBuffer(db) - const filename = backupFilename() + const filename = backupFilename(db.name) let message = 'Success' try {