import fs from 'node:fs'; import path from 'node:path'; import {createHash} from 'node:crypto'; import {DatabaseSync} from 'node:sqlite'; import {fileURLToPath} from 'node:url'; const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex'); const safeName = name => typeof name==='string' && name.length>0 && path.basename(name)===name && name!=='.' && name!=='..'; const regular = file => {if(!fs.lstatSync(file).isFile())throw new Error('Yedek yalnız normal dosyaları kabul eder.');}; function verifyDatabase(file) { const db=new DatabaseSync(file,{readOnly:true}); try { const result=db.prepare('PRAGMA integrity_check').all(); if(result.length!==1 || Object.values(result[0])[0]!=='ok')throw new Error('Veritabanı bütünlük kontrolü başarısız.'); return db.prepare('SELECT dosya FROM belgeler').all().map(r=>r.dosya); } finally {db.close();} } export function verifyUserBackup(directory) { const manifest=JSON.parse(fs.readFileSync(path.join(directory,'manifest.json'),'utf8')); if(manifest.version!==1 || !Array.isArray(manifest.files))throw new Error('Geçersiz yedek manifesti.'); const seen=new Set(); for(const entry of manifest.files) { if(entry.name!=='users.sqlite' && !(entry.name?.startsWith('belgeler/') && safeName(entry.name.slice(9))))throw new Error('Geçersiz yedek dosya yolu.'); if(seen.has(entry.name))throw new Error('Tekrarlı yedek girdisi.');seen.add(entry.name); const file=path.join(directory,entry.name);regular(file); if(hash(file)!==entry.sha256 || fs.statSync(file).size!==entry.bytes)throw new Error('Yedek dosyası eksik veya değişmiş: '+entry.name); } if(!seen.has('users.sqlite'))throw new Error('Veritabanı yedekte yok.'); for(const name of verifyDatabase(path.join(directory,'users.sqlite'))) if(!safeName(name) || !seen.has('belgeler/'+name))throw new Error('Veritabanının referans verdiği belge yedekte yok.'); return manifest; } export function backupUserData(source,destination,{offline=false}={}) { if(!offline)throw new Error('Önce uygulama ve yazan işleri durdurun; offline seçeneği gerekli.'); const dbFile=path.join(source,'users.sqlite');regular(dbFile); fs.mkdirSync(destination,{mode:0o700}); // Never overwrite an existing backup. let db; try { fs.mkdirSync(path.join(destination,'belgeler'),{mode:0o700}); db=new DatabaseSync(dbFile,{readOnly:true}); db.prepare('VACUUM INTO ?').run(path.resolve(destination,'users.sqlite'));db.close();db=null; const references=verifyDatabase(path.join(destination,'users.sqlite')); for(const name of new Set(references)) { if(!safeName(name))throw new Error('Geçersiz belge dosya adı.'); const file=path.join(source,'belgeler',name);regular(file); fs.copyFileSync(file,path.join(destination,'belgeler',name),fs.constants.COPYFILE_EXCL); } const names=['users.sqlite',...[...new Set(references)].map(name=>'belgeler/'+name)]; const files=names.map(name=>{const file=path.join(destination,name);fs.chmodSync(file,0o600);return {name,bytes:fs.statSync(file).size,sha256:hash(file)};}); fs.writeFileSync(path.join(destination,'manifest.json'),JSON.stringify({version:1,createdAt:new Date().toISOString(),scope:'users.sqlite and referenced documents',files},null,2),{mode:0o600,flag:'wx'}); return verifyUserBackup(destination); } catch(error) {db?.close();fs.rmSync(destination,{recursive:true,force:true});throw error;} } export function restoreUserData(backup,destination) { const manifest=verifyUserBackup(backup); fs.mkdirSync(destination,{mode:0o700}); // Restore only to a NEW directory. try { fs.mkdirSync(path.join(destination,'belgeler'),{mode:0o700}); for(const entry of manifest.files) { const file=path.join(destination,entry.name); fs.copyFileSync(path.join(backup,entry.name),file,fs.constants.COPYFILE_EXCL);fs.chmodSync(file,0o600); if(hash(file)!==entry.sha256)throw new Error('Geri yükleme sırasında dosya değişti.'); } verifyDatabase(path.join(destination,'users.sqlite')); return {files:manifest.files.length,destination}; }catch(error){fs.rmSync(destination,{recursive:true,force:true});throw error;} } if(process.argv[1] && path.resolve(process.argv[1])===fileURLToPath(import.meta.url)) { const [command,source,destination,flag]=process.argv.slice(2); try { if(command==='backup' && source && destination)backupUserData(source,destination,{offline:flag==='--offline'}); else if(command==='verify' && source)verifyUserBackup(source); else if(command==='restore' && source && destination)restoreUserData(source,destination); else throw new Error('Kullanım: backup --offline | verify | restore '); console.log('İşlem doğrulandı.'); }catch(error){console.error(error.message);process.exitCode=1;} }