77 lines
4.8 KiB
JavaScript
77 lines
4.8 KiB
JavaScript
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;}
|
||
}
|