Files
ihaleist/server/backup-user-data.mjs

77 lines
4.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <veri-dizini> <yeni-yedek-dizini> --offline | verify <yedek> | restore <yedek> <yeni-dizin>');
console.log('İşlem doğrulandı.');
}catch(error){console.error(error.message);process.exitCode=1;}
}