| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 |
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import {fileURLToPath} from 'node:url';
- import {FILES} from './publish-industry-ui.mjs';
- const RELEASE='ksf-board-mobile-filters-20261108-r14';
- const sha=bytes=>crypto.createHash('sha256').update(bytes).digest('hex');
- const fail=code=>{throw Object.assign(Error(code),{code});};
- function safe(root,name){
- const dest=path.resolve(root,name);if(!dest.startsWith(root+path.sep))fail('PATH_OUTSIDE_TARGET');
- let current=root;for(const part of name.split('/')){current=path.join(current,part);if(fs.existsSync(current)&&fs.lstatSync(current).isSymbolicLink())fail('SYMLINK_PATH');}
- return dest;
- }
- function read(file){if(!fs.existsSync(file))return null;const s=fs.lstatSync(file);if(!s.isFile()||s.isSymbolicLink())fail('NOT_PLAIN_FILE');return fs.readFileSync(file);}
- function digest(file){const bytes=read(file);return bytes===null?null:sha(bytes);}
- function restore(file,bytes){
- const temp=file+'.ksf-rollback-'+crypto.randomUUID();let fd;
- try{fd=fs.openSync(temp,'wx',0o644);fs.fchmodSync(fd,0o644);fs.writeFileSync(fd,bytes);fs.fsyncSync(fd);fs.closeSync(fd);fd=undefined;fs.renameSync(temp,file);}
- finally{if(fd!==undefined)fs.closeSync(fd);if(fs.existsSync(temp))fs.unlinkSync(temp);}
- }
- export function rollbackFiles({targetDir,backupDir,apply=false}){
- for(const dir of [targetDir,backupDir])if(!path.isAbsolute(dir)||fs.realpathSync(dir)!==path.resolve(dir)||dir===path.parse(dir).root)fail('UNSAFE_DIRECTORY');
- targetDir=path.resolve(targetDir);backupDir=path.resolve(backupDir);
- if(backupDir===targetDir||backupDir.startsWith(targetDir+path.sep))fail('BACKUP_INSIDE_TARGET');
- const journal=JSON.parse(read(safe(backupDir,'journal.json')));
- if(journal.release!==RELEASE||journal.targetDir!==targetDir||!Array.isArray(journal.files)||!['verified','stopped'].includes(journal.status))fail('BAD_JOURNAL');
- if(new Set(journal.files.map(f=>f.path)).size!==journal.files.length||journal.files.some(f=>!FILES.includes(f.path)||!/^[a-f0-9]{64}$/.test(f.afterSha256)||f.beforeSha256!==null&&!/^[a-f0-9]{64}$/.test(f.beforeSha256)))fail('BAD_FILE_LIST');
- const index=safe(targetDir,'index.html');if(digest(index)!==journal.homepageSha256)fail('HOMEPAGE_MISMATCH');
- const plans=journal.files.map(f=>{
- const file=safe(targetDir,f.path),current=digest(file),bytes=f.beforeSha256===null?null:read(safe(backupDir,f.path));
- if(f.beforeSha256!==null&&(bytes===null||sha(bytes)!==f.beforeSha256))fail('BACKUP_HASH_MISMATCH');
- if(current!==f.afterSha256&¤t!==f.beforeSha256)fail('CONCURRENT_CHANGE');
- return {...f,file,current,bytes,change:current!==f.beforeSha256};
- });
- const changes=plans.filter(p=>p.change);
- const result={ok:true,release:RELEASE,operation:apply?'filesystem-rollback':'read-only-rollback-plan',readOnly:!apply,targetDir,backupDir,changes:changes.map(p=>p.path),homepageModified:false,networkRequests:0};
- if(!apply||!changes.length)return {...result,filesystemVerified:apply};
- const lock=path.join(path.dirname(targetDir),'.'+path.basename(targetDir)+'.ksf-industry-ui.lock');
- const fd=fs.openSync(lock,'wx',0o600);const done=[];
- try{
- for(const p of changes){
- safe(targetDir,p.path);if(digest(index)!==journal.homepageSha256||digest(p.file)!==p.current)fail('CONCURRENT_CHANGE');
- if(p.bytes===null)fs.unlinkSync(p.file);else restore(p.file,p.bytes);
- done.push(p.path);
- }
- for(const p of plans)if(digest(p.file)!==p.beforeSha256)fail('ROLLBACK_VERIFY_FAILED');
- if(digest(index)!==journal.homepageSha256)fail('HOMEPAGE_MISMATCH');
- const receipt={...result,at:new Date().toISOString(),filesystemVerified:true,restored:done};
- fs.writeFileSync(path.join(backupDir,'rollback.json'),JSON.stringify(receipt,null,2)+'\n',{mode:0o600});return receipt;
- }catch(e){e.restored=done;throw e;}
- finally{fs.closeSync(fd);fs.unlinkSync(lock);}
- }
- if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
- try{
- let targetDir,backupDir,apply=false;const args=process.argv.slice(2);
- for(let i=0;i<args.length;i++){if(args[i]==='--target-dir'&&!targetDir)targetDir=args[++i];else if(args[i]==='--backup-dir'&&!backupDir)backupDir=args[++i];else if(args[i]==='--apply'&&!apply)apply=true;else fail('USAGE');}
- if(!targetDir||!backupDir)fail('USAGE');console.log(JSON.stringify(rollbackFiles({targetDir,backupDir,apply}),null,2));
- }catch(e){console.error(JSON.stringify({ok:false,error:{code:e.code||'ROLLBACK_STOPPED'},restored:e.restored||[]},null,2));process.exitCode=1;}
- }
|