chore: checkpoint web Blender migration source, tools and docs
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>
This commit is contained in:
70
tools/web/native-preferences.mjs
Normal file
70
tools/web/native-preferences.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
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);}
|
||||
}
|
||||
Reference in New Issue
Block a user