feat: update backup features and cron jobs
This commit is contained in:
@@ -22,7 +22,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const db = config.databases.find(d => d.id === db_id)
|
const db = config.databases.find(d => d.id === db_id)
|
||||||
if (!db) return NextResponse.json({ ok: false, error: 'Veritabanı bulunamadı.' })
|
if (!db) return NextResponse.json({ ok: false, error: 'Veritabanı bulunamadı.' })
|
||||||
|
|
||||||
const filename = backupFilename()
|
const filename = backupFilename(db.name)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Dump al
|
// 1. Dump al
|
||||||
|
|||||||
@@ -559,16 +559,16 @@ function BackupTab({ db }: { db: Db }) {
|
|||||||
<div className="rounded-xl border border-border/50 bg-black/20 p-4 space-y-3">
|
<div className="rounded-xl border border-border/50 bg-black/20 p-4 space-y-3">
|
||||||
<div className="text-[10px] font-mono text-muted uppercase tracking-wider">Google Drive Kurulumu</div>
|
<div className="text-[10px] font-mono text-muted uppercase tracking-wider">Google Drive Kurulumu</div>
|
||||||
{[
|
{[
|
||||||
{ 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: 2, text: '"APIs & Services" → "Enable APIs" → "Google Drive API" aç' },
|
||||||
{ n: 3, text: '"Credentials" → "Service Accounts" → Yeni hesap oluştur' },
|
{ n: 3, text: '"Credentials" → "Service Accounts" → Yeni hesap oluştur → JSON key indir' },
|
||||||
{ n: 4, text: 'Hesaba tıkla → "Keys" → "Add Key" → "JSON" — dosyayı indir' },
|
{ n: 4, text: 'Google Drive\'da bir klasör oluştur (örn. "VPS Backups")' },
|
||||||
{ n: 5, text: 'Google Drive\'da yeni klasör aç → Sağ tık → Paylaş' },
|
{ 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: 'Aşağıdaki e-postayı "Düzenleyici" olarak ekle ve klasör ID\'sini kopyala' },
|
{ 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 => (
|
].map(s => (
|
||||||
<div key={s.n} className="flex gap-3 text-[11px] font-mono leading-relaxed">
|
<div key={s.n} className={`flex gap-3 text-[11px] font-mono leading-relaxed ${(s as any).highlight ? 'bg-yellow-500/5 border border-yellow-500/20 rounded-lg px-2 py-1.5 -mx-2' : ''}`}>
|
||||||
<span className="w-5 h-5 rounded-full bg-accent/10 border border-accent/20 text-accent flex items-center justify-center text-[10px] shrink-0 mt-0.5">{s.n}</span>
|
<span className={`w-5 h-5 rounded-full border flex items-center justify-center text-[10px] shrink-0 mt-0.5 ${(s as any).highlight ? 'bg-yellow-500/20 border-yellow-500/40 text-yellow-400' : 'bg-accent/10 border-accent/20 text-accent'}`}>{s.n}</span>
|
||||||
<span className="text-muted/80">{s.text}</span>
|
<span className={(s as any).highlight ? 'text-yellow-200/80' : 'text-muted/80'}>{s.text}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+13
-54
@@ -14,10 +14,11 @@ export interface Database {
|
|||||||
db_type?: 'postgres' | 'mysql'
|
db_type?: 'postgres' | 'mysql'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function backupFilename(): string {
|
export function backupFilename(dbName?: string): string {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
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<Buffer> {
|
export async function createDbDumpBuffer(db: Database): Promise<Buffer> {
|
||||||
@@ -122,49 +123,6 @@ async function getGoogleAccessToken(credentials: Record<string, string>): Promis
|
|||||||
return access_token
|
return access_token
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrCreateDriveFolder(
|
|
||||||
accessToken: string,
|
|
||||||
folderName: string,
|
|
||||||
parentId?: string
|
|
||||||
): Promise<string> {
|
|
||||||
// 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<string, unknown> = {
|
|
||||||
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(
|
export async function uploadToGoogleDrive(
|
||||||
serviceAccountJsonStr: string,
|
serviceAccountJsonStr: string,
|
||||||
dbName: string,
|
dbName: string,
|
||||||
@@ -172,15 +130,16 @@ export async function uploadToGoogleDrive(
|
|||||||
content: Buffer,
|
content: Buffer,
|
||||||
parentFolderId?: string
|
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 credentials = JSON.parse(serviceAccountJsonStr)
|
||||||
const accessToken = await getGoogleAccessToken(credentials)
|
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 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([
|
const parts = Buffer.concat([
|
||||||
Buffer.from(`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${meta}\r\n`),
|
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(
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -224,17 +183,17 @@ export async function testGoogleDriveCredentials(
|
|||||||
|
|
||||||
if (folderId) {
|
if (folderId) {
|
||||||
const folderRes = await fetch(
|
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}` } }
|
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||||||
)
|
)
|
||||||
if (!folderRes.ok) {
|
if (!folderRes.ok) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
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()
|
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}` }
|
return { ok: true, detail: `Token alındı — Hesap: ${credentials.client_email}` }
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function reloadBackupCrons() {
|
|||||||
console.log(`[Backup] ${db.name} (${row.cloud_type}) başlatılıyor...`)
|
console.log(`[Backup] ${db.name} (${row.cloud_type}) başlatılıyor...`)
|
||||||
|
|
||||||
const buffer = await createDbDumpBuffer(db)
|
const buffer = await createDbDumpBuffer(db)
|
||||||
const filename = backupFilename()
|
const filename = backupFilename(db.name)
|
||||||
let message = 'Success'
|
let message = 'Success'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user