🔧 master: work in progress [might break]

This commit is contained in:
Leon van Kammen 2026-07-28 16:41:07 +02:00
parent f99c2e502f
commit 7276561d6b
9 changed files with 264 additions and 51 deletions

2
run.sh
View file

@ -1,4 +1,4 @@
#!/bin/sh
source ./.env
rebuild
result/bin/xrforge.com -c 0 "$@"
DEBUG=1 result/bin/xrforge.com -c 0 "$@"

View file

@ -15,7 +15,7 @@ HidePath('/usr/share/ssl/')
app = require("soakbean") {
bin = "./xrforge.com",
bindir = arg[-1]:gsub("xrforge.com",""),
cache = { items = {} },
cache = { items = {}, url = {} },
plugin = {},
opts = {},
forkpids = {},
@ -38,8 +38,9 @@ app.opts = require('config')(app)
-- load system plugins
app.plugin.moderation = require('plugin/moderation')
app.plugin.diskscan = require('plugin/diskscan')
app.plugin.manyfold = require('plugin/manyfold')
app.plugin.xrfragments = require('plugin/xrfragments')
app.plugin.janusxr = require('plugin/janusxr')
app.plugin.manyfold = require('plugin/manyfold')
app.plugin.git = require('plugin/git')
app.plugin.clouddrive = require('plugin/clouddrive')
app.plugin.webhook = require('plugin/webhook')
@ -59,7 +60,8 @@ end)
app.plugin[1] = app.plugin.diskscan
app.plugin[2] = app.plugin.moderation
app.plugin[3] = app.plugin.janusxr
app.plugin[4] = app.plugin.manyfold
app.plugin[4] = app.plugin.xrfragments
app.plugin[5] = app.plugin.manyfold
app.runcmd(app) -- run clicmds
@ -86,21 +88,58 @@ end)
app --
.use( require("middleware/httpauth")() )
.use( function(req,res,next)
local diskpath = app.opts.plugin_diskscan_dir
local tile = require('page/partial/tile')
local rootpath = "/" .. app.opts.plugin_diskscan_dirname
local urlpath = GetPath()
print( urlpath .. " => " .. diskpath)
foreach( app.cache.items, function(k,v)
print(v.path)
--local itempath = v.path:gsub("^.","")
--if itempath:sub(0,#diskpath) == diskpath then
-- local _, nslashes = itempath:gsub("/","")
-- local _, diskslashes = diskpath:gsub("/","")
-- if nslashes == diskslashes + 1 then
-- print("slashes = " .. nslashes .. " => " .. diskslashes)
-- table.insert(items,v)
-- end
--end
end)
local isdir = urlpath:sub(-1) == "/"
local nslashes = util.countChar('/',urlpath)
res.items = {}
local match = function(a,b) return util.stripPath(a) == util.stripPath(b) end
if urlpath:match("^"..rootpath) then
if not app.cache.url[ urlpath ] then
foreach( app.cache.items, function(k,v)
if v.parent then
trace( "path = " .. util.stripPath(v.path) .. " " ..
"parent = " .. util.stripPath(v.parent.path) .. " " ..
"urlpath = " .. util.stripPath(urlpath)
)
if match(v.path, urlpath) then -- show parent folder if any
local parent = app.cache.items[ v.parent.id ]
if v.dir then
res.items[ parent.id ] = parent.portal or parent -- portal = symlinked item
print("222222222")
end
if match(parent.path, app.opts.plugin_diskscan_dir) then -- add rootfolder if needed
local root = app.cache.items[ app.opts.plugin_diskscan_dir ]
res.items[ root.id ] = root
end
end
-- add items from dir
if match(v.parent.path, urlpath) then
local parent = app.cache.items[ v.parent.id ]
res.items[ v.id ] = v
--if parent.parent then
-- res.items[ v.id ] = v
--else
-- res.items[ parent.id ] = parent
--end
end
end
end)
local data = util.merge({items=''},{ opts = app.opts })
foreach( res.items, function(k,v)
data.items = data.items .. tile( util.flatten(v,"item") )
end)
app.cache.url[ urlpath ] = util.template( LoadAsset("page/dir.html"), data)
end
res.status(200)
res.header('content-type','text/html; charset=utf-8')
res.body( app.cache.url[ urlpath ] )
next()
else
next()
end
end)
.use( require("json").middleware() ) -- try plug'n'play json API middleware
.use( app.router( app.url ) ) -- try url router

View file

@ -6,7 +6,7 @@ return function(app)
name = "XRForge",
baseurl="/",
title="Your XR Forge",
subtitle="Local-first XR experiences for Phone, VRheadset & desktop.",
subtitle="Portable XR experiences for Phone, VRheadset & desktop.",
admin_title = "XRForge Admin",
admin_subtitle = "Manage your XR experiences here",
}

View file

@ -1,5 +1,15 @@
local util = { }
util.fileExist = function(filename)
local file, err = io.open(filename, mode or "r")
local exist = false
if file then
exist = true
file:close()
end
return exist
end
util.readfile = function(filename, mode, silent)
local file, err = io.open(filename, mode or "r")
if not file then
@ -192,10 +202,47 @@ function trace(o)
end
end
-- usage: result = filter({ {foo=1},{foo=2} }, function(k,v) return v.foo > 1 end) -- {{foo=2}}
-- result = filter({ {foo=1},{foo=2} }, {foo=1}) -- {{foo=1}}
function util.filter(t,t_or_func)
local result = {}
foreach(t, function(k,v)
if type(t_or_func) == "function" then
if t_or_func(k,v) then result[k] = v end
else
local match = 0
foreach( t_or_func, function(kk,vv)
if v[kk] == vv then result[k] = v end
end)
if match == util.count(t_or_func) then result[k] = v end
end
end)
return result
end
function util.countChar(char, str)
local _, nchars = str:gsub(char,"")
return nchars
end
function print_r(t)
print( util.dump(t) )
end
function util.split(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t = {}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
table.insert(t, str)
end
return t
end
function util.stripPath(str)
return str:gsub("^./",""):gsub("^/",""):gsub("/$","")
end
return util

View file

@ -7,7 +7,7 @@ ${header}
<hr style="margin-top:0">
<div class="grid">
${tiles}
${items}
</div>
${footer}

View file

@ -26,7 +26,7 @@
<td class="menu">
<div class="margin1L">
<!-- important: no spaces between anchor tags for older browsers without flexbox -->
<a href="/dir">/myverse</a><a href="/about">/about</a>
<a href="/${opts.plugin_diskscan_dirname}">${opts.plugin_diskscan_dirname}</a><a href="/about">about</a>
<!--<a class="xl" href="/howto">Howto</a></div>-->
</td>
</tr>

View file

@ -2,6 +2,7 @@ local dir = require "opendirectory"
app.on('init', function()
print("scanning experiences (this make take a while..)")
app.opts.plugin_diskscan_dirname = app.opts.plugin_diskscan_dir:gsub("^[.]?[/]?","")
app.plugin.diskscan.scan(app.opts.plugin_diskscan_dir)
end)
@ -27,46 +28,94 @@ return {
end,
scan = function(scandir,parent,seen)
seen = seen or {}
local seen = seen or {}
local me = app.plugin.diskscan
-- ensure rootdir as experience
app.cache.items[ scandir ] = {
path = scandir,
url = util.stripPath(scandir),
name = scandir:gsub(".*/",""),
title = scandir:gsub(".*/",""),
file = { ino = scandir },
id = scandir
}
parent = parent or app.cache.items[scandir]
-- scan dirs
local dref, derr = dir.foreach( scandir, function(path,stat,state,name,kind,ino,off,attr,fullpath)
local scanFile = function(symlinksOnly)
return function(path,stat,state,name,kind,ino,off,attr,fullpath)
if seen[ino] then return end
local experience = {
path = fullpath:gsub("^.",""), -- strip .
name = name,
title = name,
tag = {},
file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
}
local experience = {
path = fullpath,
url = util.stripPath(fullpath),
name = name,
title = name,
sidecarfile = {},
tag = {},
id = ino,
file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
}
experience.parent = parent
-- found directory
if not seen[ino] and (kind == unix.DT_DIR or kind == unix.DT_LNK)
and not me.shouldHideFile(name) then
seen[ino] = true -- prevent recursion with symlinks
print("".. fullpath)
app.pub("scan_experience", nil, experience)
table.insert( app.cache.items, experience)
me.scan(fullpath, dir,seen)
if (symlinksOnly and kind == unix.DT_LNK)
and not me.shouldHideFile(name) then
--seen[ino] = experience.file -- prevent recursion with symlinks
local stat, err = unix.stat(fullpath)
local refItem = seen[stat:ino()]
if refItem then
print(" ".. fullpath)
print("✅ └─ plugin/diskscan: detected circular symlink..creating portal")
experience.file = refItem.file
experience.portal = refItem
app.pub("scan_experience", nil, experience)
app.cache.items[ino] = experience
else
print(":(")
print("symlink: " .. fullpath)
print_r(experience)
end
end
if symlinksOnly then return end
-- found directory
if (kind == unix.DT_DIR and kind ~= unix.DT_LNK)
and not me.shouldHideFile(name) then
seen[ino] = experience -- prevent recursion with symlinks
print("".. fullpath)
experience.dir = true
app.pub("scan_experience", nil, experience)
app.cache.items[ino] = experience
me.scan(fullpath,experience,seen)
end
---- found file
if (kind ~= unix.DT_DIR and kind ~= unix.DT_LNK)
and not me.shouldHideFile(name) then
seen[ino] = experience.file -- prevent recursion with symlinks
trace(" ".. fullpath)
experience.tag.file = true
app.pub("scan_experience_file", nil, experience)
local size = attr:size()
end
end
---- found file
if not seen[ino] and (kind ~= unix.DT_DIR and kind ~= unix.DT_LNK)
and not me.shouldHideFile(name) then
seen[ino] = true -- prevent recursion with symlinks
print(" ".. fullpath)
app.pub("scan_experience_file", nil, experience)
local size = attr:size()
end
end)
end
-- first we scan real files, afterwards symlinks (to deal with circular symlinks)
local dref, derr;
print("--- no symlink")
dref, derr = dir.foreach( scandir, scanFile(false) )
if not derr then dref:close() end
print("--- symlink")
dref, derr = dir.foreach( scandir, scanFile(true) )
if not derr then dref:close() end
-- cleanup file handles
foreach( app.cache.items, function(k,v)
-- remove file attributes (which are closed already)
v.file.stat = nil
v.file.attr = nil
v.file.state = nil
end)
end
}

View file

@ -4,7 +4,6 @@ local pattern_fireboxroom = "[Ff][Ii][Rr][Ee][Bb][Oo][Xx][Rr][Oo][Oo][Mm][ ]?.*>
local pattern_title = "<[Tt][Ii][Tt][Ll][Ee]>(.*)<"
app.on('scan_experience', function(exp)
print("dir: "..exp.file.fullpath)
--print_r(exp)
end)
@ -20,7 +19,7 @@ app.on('scan_experience_file', function(exp)
exp.title = title:gsub("<.*","")
end
app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins
table.insert( app.cache.items, exp)
app.cache.items[ exp.file.ino ] = exp
print("✅ └─ plugin/janusxr: " .. exp.file.name)
end
else

View file

@ -0,0 +1,79 @@
local json = require('json')
app.on('init', function()
end)
app.on('scan_experience_file', function(exp)
local me = app.plugin.xrfragments
foreach( util.split( app.opts.plugin_xrfragments_filetypes, ' '), function(k,ext)
if exp.path:lower():match("."..ext.."$") then
if ext == 'glb' then
-- detect XR Fragment exp https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File
local tree = me.extract_glb_json( util.stripPath(exp.path) )
local hasHref = me.detectHrefInStruct(tree)
end
-- check sidecarfiles: https://xrfragment.org/#sidecar%20files
local sidecarExts = {"png","vtt","ogg","json"}
foreach( sidecarExts, function(k,sidecarExt)
if util.fileExist( exp.path:gsub("."..ext.."$", "."..sidecarExt) ) then
exp.sidecarfile[ sidecarExt ] = true
exp.view3D = true
end
end)
local jsonfile = exp.path:gsub("."..ext.."$",".json")
-- scan for hrefs in json sidecarfile
if util.fileExist( jsonfile ) then
exp.sidecarfile.JSONXRF = me.detectHrefInStruct( json.decode( util.readfile(jsonfile) ) )
end
exp.tag.xrfragments = hasPNG or hasWebVTT or hasOGG or hasJSONref
if exp.tag.xrfragments then
print("✅ └─ plugin/xrfragments: detected xrfragments experience")
end
foreach(exp.sidecarfile, function(ext,v)
print("✅ └─ plugin/xrfragments: detected .".. ext:lower() .. " sidecarfile")
end)
-- finally insert if viewable
if exp.view3D then
app.cache.items[ exp.file.ino ] = exp
end
end
end)
end)
-- this registers the plugin UI /funcs
return {
name = "XR Fragments",
info = '<a href="https://xrfragment.org" target="_blank">deeplinking for 3D files</a>',
opts = {
{
key='app.opts.plugin_xrfragments_filetypes',
title='Filetypes',
info='Space-separated 3D filetypes, <a href="http://lua-users.org/wiki/PatternsTutorial" target="_blank">lua patterns</a> are allowed',
default="glb gltf obj dae",
values=''
}
},
extract_glb_json = function(filename)
local f = assert(io.open(filename, "rb"))
-- Skip 12-byte header + 4-byte chunk length + 4-byte chunk type
f:seek("set", 12)
local length_data = f:read(4)
if not length_data then f:close(); return nil end
-- Unpack 32-bit little-endian integer for the JSON chunk length
local length = string.unpack("<I4", length_data)
f:seek("set", 20) -- Jump straight to the start of the JSON string
local json_str = f:read(length)
f:close()
return json.decode(json_str)
end,
detectHrefInStruct = function(t,parent,parentKey,result)
result = result or {detected=false}
foreach( t, function(k,v)
if k == 'href' then result.detected = true end
if type(v) == 'table' then app.plugin.xrfragments.detectHrefInStruct(v,t,k,result) end
end)
return result.detected
end
}