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

106 lines
6.1 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 regular=file=>{if(!fs.lstatSync(file).isFile())throw new Error('Normal dosya gerekli: '+file);};
const inside=(root,target)=>{const relative=path.relative(path.resolve(root),path.resolve(target));return relative==='' || (!relative.startsWith('..'+path.sep)&&relative!=='..'&&!path.isAbsolute(relative));};
function safeFile(root,name) {
if(typeof name!=='string'||!name||path.isAbsolute(name)||name.split('/').some(p=>!p||p==='.'||p==='..')||name.includes('\\'))throw new Error('Geçersiz manifest yolu.');
let current=root;
for(const part of name.split('/')) {current=path.join(current,part);if(fs.existsSync(current)&&fs.lstatSync(current).isSymbolicLink())throw new Error('Yedekte sembolik bağlantı kullanılamaz.');}
return current;
}
async function hash(file) {
const digest=createHash('sha256');
for await(const chunk of fs.createReadStream(file))digest.update(chunk);
return digest.digest('hex');
}
function sqlite(file) {
const fd=fs.openSync(file,'r'),header=Buffer.alloc(16);
try {fs.readSync(fd,header,0,16,0);}finally {fs.closeSync(fd);}
return header.equals(Buffer.from('SQLite format 3\0'));
}
function integrity(file) {
const db=new DatabaseSync(file,{readOnly:true});
try {const rows=db.prepare('PRAGMA integrity_check').all();if(rows.length!==1||Object.values(rows[0])[0]!=='ok')throw new Error('SQLite bütünlük kontrolü başarısız.');}
finally {db.close();}
}
function inventory(root,relative='') {
const result=[];
for(const entry of fs.readdirSync(path.join(root,relative),{withFileTypes:true})) {
const name=relative?relative+'/'+entry.name:entry.name;
if(entry.isSymbolicLink())throw new Error('Veri volume içinde sembolik bağlantı var: '+name);
if(entry.isDirectory())result.push(...inventory(root,name));
else if(entry.isFile())result.push(name);
else throw new Error('Desteklenmeyen veri dosyası: '+name);
}
return result.sort();
}
export async function verifyDataVolume(backup) {
if(fs.lstatSync(backup).isSymbolicLink())throw new Error('Yedek dizini bağlantı olamaz.');
const manifestPath=safeFile(backup,'volume-manifest.json');regular(manifestPath);
const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));
if(manifest.version!==1||manifest.scope!=='data-volume'||!Array.isArray(manifest.files))throw new Error('Geçersiz volume manifesti.');
const names=new Set();
for(const entry of manifest.files) {
if(names.has(entry.name)||entry.name==='volume-manifest.json')throw new Error('Tekrarlı/geçersiz manifest girdisi.');names.add(entry.name);
const file=safeFile(backup,entry.name);regular(file);
if(fs.statSync(file).size!==entry.bytes||await hash(file)!==entry.sha256)throw new Error('Yedek değişmiş veya eksik: '+entry.name);
if(entry.sqlite)integrity(file);
}
const actual=inventory(backup).filter(n=>n!=='volume-manifest.json');
if(actual.length!==names.size||actual.some(n=>!names.has(n)))throw new Error('Manifest dışında dosya var.');
return manifest;
}
export async function backupDataVolume(source,destination,{offline=false}={}) {
if(!offline)throw new Error('Uygulamayı ve tüm yazan işleri durdurun; --offline gerekli.');
if(inside(source,destination)||inside(destination,source))throw new Error('Kaynak ve hedef iç içe olamaz.');
if(fs.lstatSync(source).isSymbolicLink())throw new Error('Kaynak gerçek volume dizini olmalı.');
const names=inventory(source);
if(names.includes('volume-manifest.json'))throw new Error('Kaynak bir yedek dizini gibi görünüyor.');
const dbNames=new Set(names.filter(name=>sqlite(path.join(source,name))));
fs.mkdirSync(destination,{mode:0o700});
try {
const files=[];
for(const name of names) {
if(/-(wal|shm)$/.test(name)&&dbNames.has(name.replace(/-(wal|shm)$/,'')))continue;
const original=safeFile(source,name),output=safeFile(destination,name);
fs.mkdirSync(path.dirname(output),{recursive:true,mode:0o700});
if(dbNames.has(name)) {
const db=new DatabaseSync(original,{readOnly:true});
try {db.prepare('VACUUM INTO ?').run(path.resolve(output));}finally {db.close();}
integrity(output);
} else fs.copyFileSync(original,output,fs.constants.COPYFILE_EXCL);
fs.chmodSync(output,0o600);
files.push({name,sqlite:dbNames.has(name),bytes:fs.statSync(output).size,sha256:await hash(output)});
}
fs.writeFileSync(path.join(destination,'volume-manifest.json'),JSON.stringify({version:1,scope:'data-volume',createdAt:new Date().toISOString(),files},null,2),{flag:'wx',mode:0o600});
return await verifyDataVolume(destination);
} catch(error) {fs.rmSync(destination,{recursive:true,force:true});throw error;}
}
export async function restoreDataVolume(backup,destination) {
const manifest=await verifyDataVolume(backup);
if(inside(backup,destination)||inside(destination,backup))throw new Error('Yedek ve hedef iç içe olamaz.');
fs.mkdirSync(destination,{mode:0o700});
try {
for(const entry of manifest.files) {
const target=safeFile(destination,entry.name);fs.mkdirSync(path.dirname(target),{recursive:true,mode:0o700});
fs.copyFileSync(safeFile(backup,entry.name),target,fs.constants.COPYFILE_EXCL);fs.chmodSync(target,0o600);
if(await hash(target)!==entry.sha256)throw new Error('Geri yüklenen dosya doğrulanamadı.');
if(entry.sqlite)integrity(target);
}
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)await backupDataVolume(source,destination,{offline:flag==='--offline'});
else if(command==='verify'&&source)await verifyDataVolume(source);
else if(command==='restore'&&source&&destination)await restoreDataVolume(source,destination);
else throw new Error('backup <source> <new-backup> --offline | verify <backup> | restore <backup> <new-volume>');
console.log('Volume işlemi doğrulandı.');
}catch(error){console.error(error.message);process.exitCode=1;}
}