59 lines
3.7 KiB
JavaScript
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
|
|
}
|