Files
MikrotikManager/backend/scripts/writer-profile/operator-files.mjs
T

59 lines
3.7 KiB
JavaScript

import assert from 'node:assert/strict'
import {constants} from 'node:fs'
import {open,lstat,realpath,link,unlink} from 'node:fs/promises'
import path from 'node:path'
import {createHash} from 'node:crypto'
export const sha=bytes=>createHash('sha256').update(bytes).digest('hex')
export async function protectedDirectory(directory){
assert(process.platform!=='win32'&&typeof process.getuid==='function','Native Windows ACL backend unavailable')
assert(path.isAbsolute(directory)&&await realpath(directory)===directory)
const uid=process.getuid(),s=await lstat(directory)
assert(s.isDirectory()&&!s.isSymbolicLink()&&s.uid===uid&&(s.mode&0o077)===0)
for(let p=path.dirname(directory);;p=path.dirname(p)){const s=await lstat(p);assert(s.isDirectory()&&!s.isSymbolicLink()&&(s.uid===uid||s.uid===0));assert((s.mode&0o022)===0||s.uid===0&&(s.mode&0o1000)!==0);if(p===path.dirname(p))break}
}
export async function readProtected(file,maximum,expectedSha){
await protectedDirectory(path.dirname(file));assert(await realpath(file)===file)
const f=await open(file,constants.O_RDONLY|constants.O_NOFOLLOW)
try{
const a=await f.stat();assert(a.isFile()&&a.uid===process.getuid()&&(a.mode&0o077)===0&&a.size>0&&a.size<=maximum)
const out=Buffer.alloc(a.size+1);let size=0
while(size<out.length){const r=await f.read(out,size,out.length-size,size);if(!r.bytesRead)break;size+=r.bytesRead}
const b=await f.stat();assert(size===a.size&&a.size===b.size&&a.mtimeMs===b.mtimeMs&&a.ctimeMs===b.ctimeMs)
const bytes=out.subarray(0,size);if(expectedSha!==undefined)assert(sha(bytes)===expectedSha,'Independently pinned file mismatch');return bytes
}finally{await f.close()}
}
export async function writeFile(file,bytes,options){
assert(options?.flag==='wx'&&options.mode===0o600);const parent=path.dirname(file);await protectedDirectory(parent)
const next=file+'.pending',f=await open(next,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600)
try{await f.writeFile(bytes);await f.sync()}finally{await f.close()}
await link(next,file);await unlink(next)
const directory=await open(parent,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW);try{await directory.sync()}finally{await directory.close()}
}
/** One cooperative operator owns this journal through SQL AND post-ACK cleanup.
* EEXIST/crash residue refuses; never infer stale ownership from PID or time. */
export async function withJournalLease(directory,work){
await protectedDirectory(directory)
const filename=path.join(directory,'publisher-lease.lock')
const handle=await open(filename,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600)
let identity,result,primary,failed=false,cleanupFailed=false
const syncDirectory=async()=>{const d=await open(directory,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW);try{await d.sync()}finally{await d.close()}}
try{
identity=await handle.stat({bigint:true});await handle.writeFile('mmapp-writer-journal-lease-v1\n');await handle.sync();await syncDirectory()
result=await work()
}catch(error){primary=error;failed=true}
finally{
try{
await protectedDirectory(directory)
const current=await lstat(filename,{bigint:true})
if(!identity||!current.isFile()||current.isSymbolicLink()||current.dev!==identity.dev||current.ino!==identity.ino)throw Error('Journal lease identity changed')
await unlink(filename);await syncDirectory()
}catch(error){cleanupFailed=true;primary??=error}
try{await handle.close()}catch(error){cleanupFailed=true;primary??=error}
}
// Known COMMIT remains known if the filesystem cleanup fails later.
if(!failed&&result?.outcome==='committed'&&cleanupFailed)return Object.freeze({...result,journalUnconfirmed:true})
if(failed||cleanupFailed)throw primary
return result
}