From 7276561d6b29d78b056f3e6d891d811ae602846d Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Tue, 28 Jul 2026 16:41:07 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20master:=20work=20in=20progress?= =?UTF-8?q?=20[might=20break]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.sh | 2 +- src/.init.lua | 73 ++++++++++++++++++++------ src/.lua/config.lua | 2 +- src/.lua/util.lua | 47 +++++++++++++++++ src/page/dir.html | 2 +- src/page/header.html | 2 +- src/plugin/diskscan.lua | 105 +++++++++++++++++++++++++++---------- src/plugin/janusxr.lua | 3 +- src/plugin/xrfragments.lua | 79 ++++++++++++++++++++++++++++ 9 files changed, 264 insertions(+), 51 deletions(-) create mode 100644 src/plugin/xrfragments.lua diff --git a/run.sh b/run.sh index d1343fc..c33553c 100755 --- a/run.sh +++ b/run.sh @@ -1,4 +1,4 @@ #!/bin/sh source ./.env rebuild -result/bin/xrforge.com -c 0 "$@" +DEBUG=1 result/bin/xrforge.com -c 0 "$@" diff --git a/src/.init.lua b/src/.init.lua index 08a569e..3a10783 100644 --- a/src/.init.lua +++ b/src/.init.lua @@ -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 diff --git a/src/.lua/config.lua b/src/.lua/config.lua index a643a7f..12a8140 100644 --- a/src/.lua/config.lua +++ b/src/.lua/config.lua @@ -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", } diff --git a/src/.lua/util.lua b/src/.lua/util.lua index 34f77b7..4cd661a 100644 --- a/src/.lua/util.lua +++ b/src/.lua/util.lua @@ -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 diff --git a/src/page/dir.html b/src/page/dir.html index e200a00..4f84333 100644 --- a/src/page/dir.html +++ b/src/page/dir.html @@ -7,7 +7,7 @@ ${header}
- ${tiles} + ${items}
${footer} diff --git a/src/page/header.html b/src/page/header.html index 6654ab7..cc3f0e5 100644 --- a/src/page/header.html +++ b/src/page/header.html @@ -26,7 +26,7 @@
- /myverse/about + ${opts.plugin_diskscan_dirname}about diff --git a/src/plugin/diskscan.lua b/src/plugin/diskscan.lua index 35bb1df..3860293 100644 --- a/src/plugin/diskscan.lua +++ b/src/plugin/diskscan.lua @@ -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 } diff --git a/src/plugin/janusxr.lua b/src/plugin/janusxr.lua index 5400501..c876d71 100644 --- a/src/plugin/janusxr.lua +++ b/src/plugin/janusxr.lua @@ -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 diff --git a/src/plugin/xrfragments.lua b/src/plugin/xrfragments.lua new file mode 100644 index 0000000..7436602 --- /dev/null +++ b/src/plugin/xrfragments.lua @@ -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 = 'deeplinking for 3D files', + opts = { + { + key='app.opts.plugin_xrfragments_filetypes', + title='Filetypes', + info='Space-separated 3D filetypes, lua patterns 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("