Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
71 lines
4.3 KiB
JavaScript
71 lines
4.3 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import crypto from 'node:crypto';
|
|
|
|
const profile = path.join(process.env.WEB_BLENDER_PROFILE_ROOT ?? path.join(os.homedir(), '.local', 'share', 'web-blender', '5.2'), 'config');
|
|
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
export function nativeProfileDirectories(){
|
|
const result={};
|
|
for(const name of ['scripts','extensions','datafiles']){
|
|
const directory=path.join(path.dirname(profile),name);fs.mkdirSync(directory,{recursive:true,mode:0o700});result[name]=directory;
|
|
}
|
|
return result;
|
|
}
|
|
function readSaved() {
|
|
const file = path.join(profile, 'userpref.blend');
|
|
if (!fs.existsSync(file)) return null;
|
|
const stat = fs.lstatSync(file);
|
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024 * 1024) throw new Error('PREFERENCES_FILE_INVALID');
|
|
return fs.readFileSync(file);
|
|
}
|
|
|
|
export function prepareNativePreferences(workspace) {
|
|
const config = path.join(workspace, 'config');
|
|
if(fs.existsSync(config)){const stat=fs.lstatSync(config);if(!stat.isDirectory()||stat.isSymbolicLink())throw new Error('PREFERENCES_DIRECTORY_INVALID');}else fs.mkdirSync(config,{mode:0o700});
|
|
for(const name of ['userpref.blend','startup.blend']){const file=path.join(config,name);if(fs.existsSync(file)){const stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink())throw new Error('PREFERENCES_WORKSPACE_FILE_INVALID');}}
|
|
const bytes = readSaved();
|
|
if (bytes) fs.writeFileSync(path.join(config, 'userpref.blend'), bytes);
|
|
const startup=path.join(profile,'startup.blend');let startupSha256=null;
|
|
if(fs.existsSync(startup)){
|
|
const stat=fs.lstatSync(startup);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>256*1024*1024)throw new Error('STARTUP_PROFILE_INVALID');
|
|
const data=fs.readFileSync(startup);startupSha256=digest(data);fs.writeFileSync(path.join(config,'startup.blend'),data);
|
|
}
|
|
fs.writeFileSync(path.join(workspace, 'preferences-base.json'), JSON.stringify({sha256: bytes ? digest(bytes) : null,startupSha256}));
|
|
return config;
|
|
}
|
|
|
|
export function persistNativePreferences(workspace, artifact) {
|
|
fs.mkdirSync(profile, {recursive: true, mode: 0o700});
|
|
const lock = path.join(profile, '.save-lock');
|
|
try { fs.mkdirSync(lock); } catch { throw new Error('PREFERENCES_SAVE_BUSY_RETRY'); }
|
|
try {
|
|
const base = JSON.parse(fs.readFileSync(path.join(workspace, 'preferences-base.json'), 'utf8'));
|
|
const previous = readSaved();
|
|
if ((previous ? digest(previous) : null) !== base.sha256) throw new Error('PREFERENCES_CHANGED_IN_ANOTHER_SESSION_RECONNECT');
|
|
const bytes = fs.readFileSync(path.join(workspace, artifact.id));
|
|
if (bytes.length > 16 * 1024 * 1024 || digest(bytes) !== artifact.sha256) throw new Error('PREFERENCES_ARTIFACT_INVALID');
|
|
const temporary = path.join(profile, crypto.randomUUID() + '.tmp');
|
|
try {
|
|
fs.writeFileSync(temporary, bytes, {mode: 0o600, flag: 'wx'});
|
|
fs.renameSync(temporary, path.join(profile, 'userpref.blend'));
|
|
} finally { fs.rmSync(temporary, {force: true}); }
|
|
fs.writeFileSync(path.join(workspace, 'preferences-base.json'), JSON.stringify({...base,sha256: artifact.sha256}));
|
|
} finally { fs.rmdirSync(lock); }
|
|
}
|
|
|
|
export function persistNativeStartup(workspace){
|
|
fs.mkdirSync(profile,{recursive:true,mode:0o700});const lock=path.join(profile,'.save-lock');
|
|
try{fs.mkdirSync(lock);}catch{throw new Error('PREFERENCES_SAVE_BUSY_RETRY');}
|
|
try{
|
|
const baseFile=path.join(workspace,'preferences-base.json'),base=JSON.parse(fs.readFileSync(baseFile,'utf8'));
|
|
const destination=path.join(profile,'startup.blend'),source=path.join(workspace,'config','startup.blend');
|
|
const current=fs.existsSync(destination)?digest(fs.readFileSync(destination)):null;
|
|
if(current!==(base.startupSha256??null))throw new Error('STARTUP_CHANGED_IN_ANOTHER_SESSION_RECONNECT');
|
|
const stat=fs.lstatSync(source);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>256*1024*1024)throw new Error('STARTUP_PROFILE_INVALID');
|
|
const data=fs.readFileSync(source),temporary=path.join(profile,crypto.randomUUID()+'.tmp');
|
|
try{fs.writeFileSync(temporary,data,{mode:0o600,flag:'wx'});fs.renameSync(temporary,destination);}finally{fs.rmSync(temporary,{force:true});}
|
|
fs.writeFileSync(baseFile,JSON.stringify({...base,startupSha256:digest(data)}));
|
|
}finally{fs.rmdirSync(lock);}
|
|
}
|