From 7cd6df24c72faf22cb1375d7c7f298ffb45c2607 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Mon, 3 Aug 2026 19:19:07 +0200 Subject: [PATCH] various bugfixes + clouddrive update --- .env | 4 +- README.md | 61 ++++++++++++++ src/.init.lua | 11 ++- src/.lua/soakbean.lua | 2 +- src/.lua/util.lua | 39 ++++++++- src/cmd/generate.lua | 4 +- src/css/xrforge.css | 6 +- src/federation.json | 4 + src/page/admin/index.lua | 24 +++--- src/page/admin/plugin.html | 20 +++-- src/page/dir.lua | 42 +++++----- src/page/footer.janusxr.html | 6 +- src/plugin/clouddrive.lua | 154 ++++++++++++++++++++++------------ src/plugin/diskscan.lua | 12 +-- src/plugin/federation.lua | 76 +++++++++++++++-- src/plugin/moderation.lua | 18 +++- {util => src/util}/xrforge.sh | 0 17 files changed, 366 insertions(+), 117 deletions(-) create mode 100644 src/federation.json rename {util => src/util}/xrforge.sh (100%) diff --git a/.env b/.env index 1afc1dd..f0395e0 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ # aliases -build(){ $(pwd)/util/xrforge.sh build; } -rebuild(){ $(pwd)/util/xrforge.sh rebuild; } +build(){ $(pwd)/src/util/xrforge.sh build; } +rebuild(){ $(pwd)/src/util/xrforge.sh rebuild; } # forgejo server hook #test "$GITEA_ROOT_URL" = "https://xrforge.isvery.ninja/" && { diff --git a/README.md b/README.md index 5c6f312..913fae6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,65 @@ XRForge is a tiny, powerful tool to interconnect XR experiences.
+
+An open, seamless XR web built from simple URLs and directories, without gatekeepers.
+
+
+XRForge transforms standard local or remote file directories into interconnected 3D spaces. +
Users can seamlessly "surf" the spatial web, turning everyday directories into 3D portals. + +``` + ┌───────────────────────────────────┐ ┌──────────────────────────────────────────┐ + │ local disk │ │ Surf via 3D portals │ + ├───────────────────────────────────┤ └──────────────────────────────────────────┘ + │ │ + │ /myverse │ https://my.org/myverse + │ /elearnings │ https://my.org/myverse/elearnings + │ /elearning1/1.glb │ https://my.org/myverse/elearning/elearning1 + │ /elearning2/2.glb │ https://my.org/myverse/elearning/elearning2 + │ │ + │ ┌──────────────────────────────┐ │ + │ │ github / webdav / dropbox etc│ │ + │ │ │ │ + │ │ /mygames │ │ https://my.org/myverse/mygames + │ │ /biggusgateWebXR │ │ https://my.org/myverse/mygames/biggusgate + │ │ /spasca │ │ https://my.org/myverse/mygames/spasca + │ │ /otherxrforge │ │ https://my.org/myverse/otherxrforge + │ │ │ │ + │ │ ┌───────────┐ ┌───────────┐ │ │ + │ │ │/manyfold │ │archive.org│ │ │ https://my.org/myverse/manyfold + │ │ └───────────┘ └───────────┘ │ │ https://my.org/myverse/myarchive + │ │ │ │ + │ │ +40 integrations! │ │ + │ └──────────────────────────────┘ │ + └───────────────────────────────────┘ +``` + + +## Usage / Radical Opensource + +```bash +$ ./xrforge.com +Usage: ./xrforge.com [opts] + + ./xrforge.com test run testsuite (selftest) + ./xrforge.com admin launch adminpanel to configure stuff (/admin) + ./xrforge.com generate generate www-folder with XR experiences + ./xrforge.com help display all possible flags + ./xrforge.com createplugin generates skeleton plugin + ./xrforge.com server starts the webserver [without --nobrowser] + ./xrforge.com build build + +$ unzip xrforge.com +Archive: result/bin/xrforge.com + Length Date Time Name +--------- ---------- ----- ---- + 3992 08-03-2026 09:54 .init.lua + 242 08-03-2026 09:46 cmd/server.lua + 1014 07-23-2026 16:13 cmd/createplugin.lua + 580 07-24-2026 16:12 cmd/help.lua + .... +``` + +> Profit! you have aqcuired the sourcecode from the binary: run and distribute modified versions if needed ♥ ## Adding content diff --git a/src/.init.lua b/src/.init.lua index 07bf501..0277a9c 100644 --- a/src/.init.lua +++ b/src/.init.lua @@ -43,6 +43,8 @@ app = require("soakbean") { end } +util.fetchInstall(app) -- setup internal fetch callback mechanism + -- api skeleton (implemented via soakbean's app.on(...)) app.set = function(k,v) end app.init = function() end @@ -55,10 +57,10 @@ app.plugin.diskscan = app.addPlugin('plugin/diskscan') app.plugin.moderation = app.addPlugin('plugin/moderation') app.plugin.xrfragments = app.addPlugin('plugin/xrfragments') app.plugin.janusxr = app.addPlugin('plugin/janusxr') -app.plugin.xrforges = app.addPlugin('plugin/xrforges') +app.plugin.clouddrive = app.addPlugin('plugin/clouddrive') +app.plugin.federation = app.addPlugin('plugin/federation') app.plugin.manyfold = app.addPlugin('plugin/manyfold') app.plugin.git = app.addPlugin('plugin/git') -app.plugin.clouddrive = app.addPlugin('plugin/clouddrive') app.plugin.webhook = app.addPlugin('plugin/webhook') app.plugin.markdown = app.addPlugin('plugin/markdown') @@ -104,6 +106,11 @@ end) function OnHttpRequest() app.run() end +OnServerListen = function(socketdescriptor, serverip, serverport) + app.ip = FormatIp(serverip) + app.port = serverport +end + OnServerStart = function() app.init() -- this triggers init event end diff --git a/src/.lua/soakbean.lua b/src/.lua/soakbean.lua index a6ecc7b..5d15066 100644 --- a/src/.lua/soakbean.lua +++ b/src/.lua/soakbean.lua @@ -188,7 +188,7 @@ sb = { header=GetHeaders(), url=GetPath(), protocol=GetScheme(), - body={} + body=GetBody(), } local res={ _status=nil, diff --git a/src/.lua/util.lua b/src/.lua/util.lua index bd64fe4..2aa5831 100644 --- a/src/.lua/util.lua +++ b/src/.lua/util.lua @@ -75,10 +75,17 @@ util.templateFile = function(file,data) return util.template( LoadAsset(file), data) end -util.getURL = function() - local port = ':' .. GetPort() - if port == 80 then port = '' end - return GetScheme() .. '://' .. GetHost() .. port .. GetPath() +util.getURL = function(url,opts) + opts = opts or {} + local port = '' + if opts.server then + url = url or '/' + return 'http://localhost:' .. app.port .. url + else + port = ':' .. GetPort() + if port == 80 then port = '' end + return GetScheme() .. '://' .. GetHost() .. port .. (url or GetPath()) + end end util.template = function(content,data) @@ -287,6 +294,30 @@ function util.pluck(path, tbl, default) return current end +function util.fetchInstall(app) + app.expectedreq = {} + app.post('/bus',function(req,res,next) + local reqid = GetParam('id') + local cb = app.expectedreq[reqid] + if cb then cb(req,res,next) end + end) +end + +function util.fetch(url, opts, cb) + local reqid = tostring( Rdrand() ) + app.expectedreq[reqid] = cb + local urlret = util.getURL('/bus?id=' .. reqid, {server=true} ) + if assert(unix.fork()) == 0 then + local ok, headers, body = Fetch(url, opts) + if ok then + Fetch(urlret, body) -- post back to ourselves + else + print(headers) + end + unix.exit(0) + end +end + function util.stripPath(str) return str:gsub("^./",""):gsub("^/",""):gsub("/$","") diff --git a/src/cmd/generate.lua b/src/cmd/generate.lua index 4d694ea..57a050e 100644 --- a/src/cmd/generate.lua +++ b/src/cmd/generate.lua @@ -1,4 +1,6 @@ return function(app,argv) - app.plugin.diskscan.scan() + print('generating..') + app.plugin.clouddrive.mount() + --app.plugin.diskscan.scan( app.opts.plugin_diskscan_dir ) os.exit() end diff --git a/src/css/xrforge.css b/src/css/xrforge.css index cbca605..608794f 100644 --- a/src/css/xrforge.css +++ b/src/css/xrforge.css @@ -270,8 +270,12 @@ img.icon{ display:none; } .info{ background:#224; - padding:10px; + padding:10px 20px; + overflow:clip; } +.info .tag{ + margin-bottom:7px; +} b.tag{ display:inline-block; diff --git a/src/federation.json b/src/federation.json new file mode 100644 index 0000000..7fb9503 --- /dev/null +++ b/src/federation.json @@ -0,0 +1,4 @@ +[ + {"url":"https://xrforge.isvery.ninja", "name":"XRForge", "thumbnail":""}, + {"url":"https://www.janusxr.org", "name":"JanusXR", "thumbnail":""} +] diff --git a/src/page/admin/index.lua b/src/page/admin/index.lua index cb5a3cf..761813b 100644 --- a/src/page/admin/index.lua +++ b/src/page/admin/index.lua @@ -1,6 +1,6 @@ local util = require("util") -local data = util.merge({notification=''},{ opts = app.opts }) +local data = {notification=''} data.plugins = "" data.tabs = {} @@ -15,13 +15,13 @@ function pluginInput(opt) ]] - local value = app.opts[ opt.key:gsub("^app.opts.","") ] + local key = opt.key:gsub("^app.opts.","") + local value = app.opts[ key ] if type(opt.values) == 'table' then html = html .. util.template('\n" else - html = html .. util.template('',{ + html = html .. util.template('',{ key = opt.key, - value = value + value = value, + placeholder= opt.placeholder or '' }) end html = html .. util.template([[ ${info} - ]],opt) return html end -function pluginUI(seen) +function pluginUI(seen,data) return function(name,plugin) if seen[ plugin.name ] then return end -- already processed table.insert( data.tabs, plugin.name ) -- insert tabname @@ -63,8 +63,8 @@ function saveData() local params = GetParams() local saveconfig = false foreach(params, function(k,v) - if v[1]:match("^opts.") then - app.opts[ v[1]:gsub("^opts.","") ] = v[2] -- update + if v[1]:match("^app%.opts%.") then + app.opts[ v[1]:gsub("^app%.opts%.","") ] = v[2] -- update saveconfig = true end end) @@ -77,9 +77,11 @@ function saveData() end end -local seen = {} -foreach( app.plugin, pluginUI(seen) ) saveData() + +data = util.merge(data,{ opts = app.opts }) +local seen = {} +foreach( app.plugin, pluginUI(seen,data) ) local html = util.template( LoadAsset("page/admin/index.html"),data) diff --git a/src/page/admin/plugin.html b/src/page/admin/plugin.html index 9a5bc3c..b426fd3 100644 --- a/src/page/admin/plugin.html +++ b/src/page/admin/plugin.html @@ -15,11 +15,21 @@ {if #plugin.info > 2 then - print([[ -
- ${plugin.info} -
- ]]) + if plugin.infowide then + print([[ + + +
+ ${plugin.info} +
+ ]]) + else + print([[ +
+ ${plugin.info} +
+ ]]) + end end}

diff --git a/src/page/dir.lua b/src/page/dir.lua index 6819ddd..d4eec15 100644 --- a/src/page/dir.lua +++ b/src/page/dir.lua @@ -21,29 +21,30 @@ return { local matchpath = urlpath if urlpath == '/' then matchpath = rootpath end -- generate janusxr footer for root url foreach( app.cache.items, function(k,v) - if v.parent then - trace( "path = " .. util.stripPath(v.path) .. " " .. - "parent = " .. util.stripPath(v.parent.path) .. " " .. - "matchpath = " .. util.stripPath(matchpath) - ) - if match(v.path, matchpath) then -- show parent folder if any - local parent = app.cache.items[ v.parent.id ] - if v.portal then - redirect = v.portal.url + if req.authenticated or app.plugin.moderation.isAllowed(v) then + if v.parent then + trace( "path = " .. util.stripPath(v.path) .. " " .. + "parent = " .. util.stripPath(v.parent.path) .. " " .. + "matchpath = " .. util.stripPath(matchpath) + ) + if match(v.path, matchpath) then -- show parent folder if any + local parent = app.cache.items[ v.parent.id ] + if v.portal then + redirect = v.portal.url + 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 - 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 + + -- add items from dir + if match(v.parent.path, matchpath) and (v.dir or v.view3D) then + local parent = app.cache.items[ v.parent.id ] + res.items[ v.id ] = v + res.parent = parent end end - - -- add items from dir - if match(v.parent.path, matchpath) and (v.dir or v.view3D) then - local parent = app.cache.items[ v.parent.id ] - res.items[ v.id ] = v - res.parent = parent - end - end end) local data = util.merge({ @@ -72,6 +73,7 @@ return { if data.item then res.template = 'page/index.html' end + app.pub("dir", nil, {res=res,data=data, urlpath=urlpath, isdir=isdir}) app.cache.url[ urlpath ] = util.templateFile( res.template, data) end if redirect then diff --git a/src/page/footer.janusxr.html b/src/page/footer.janusxr.html index 834ead2..a0b32ec 100644 --- a/src/page/footer.janusxr.html +++ b/src/page/footer.janusxr.html @@ -7,9 +7,9 @@ end end} - + - + {if item then print('\t') end} @@ -22,7 +22,7 @@ local center = -((#items*width)/1.3) + (i*width) local url = v.url if v.dir then url = url .. "/" end - print('\t\t') + print('\t\t') print('\t\t ') print('\t\t ') print('\t\t ') diff --git a/src/plugin/clouddrive.lua b/src/plugin/clouddrive.lua index 741a630..fdd7857 100644 --- a/src/plugin/clouddrive.lua +++ b/src/plugin/clouddrive.lua @@ -1,5 +1,5 @@ local util = require('util') -local remotes = {"-- disabled --"} +local dir = require('opendirectory') app.on('scan_experience', function(exp) --print_r(exp) @@ -12,65 +12,113 @@ app.on('set', function(k,v) print("content was updated") end) -if util.cmdExist('rclone') then - local stdout = util.execute('rclone listremotes 2>&1') - print('stdout = ' .. stdout) +-- this registers the plugin UI +local plugin = { + name = 'Cloud drives', + info = '', + infowide = true, + opts = { }, + mount = function() + if util.cmdExist('rclone') then + local me = app.plugin.clouddrive + for i=1,5 do + local k = 'plugin_clouddrive_remote_' .. i + local mapping = app.opts[k] + if mapping and #mapping > 0 and not mapping:match("%.%.") then + local dir = '.' .. mapping:gsub(".* ","") + local remote = mapping:gsub(" .*","") + os.execute('test -d ' .. dir .. ' || mkdir ' .. dir) + local cmd = 'rclone mount --no-checksum --no-modtime --no-seek --allow-other --daemon ' .. remote .. ' ' .. dir + stdout, retstr, retcode = os.execute('mountpoint ' .. dir ) + if retcode ~= 0 then + print("please run: " .. cmd) + end + end + end + else + print('[!] rclone not detected..will skip mounting remotes') + end + end +} + +function scanRemotes(remotes) + local stdout, retstr, retcode = util.execute('rclone listremotes 2>&1') local lines = util.split(stdout,"\n") foreach( lines, function(k,v) local remote = v:gsub(":$","") remotes[ remote ] = remote end) + if util.count(remotes) > 0 then + plugin.info = plugin.info .. '

rclone remotes detected:

' + foreach(remotes, function(k,remote) + plugin.info = plugin.info .. '' .. remote .. ':/ ' + end) + end end +function scanMountables(mountables) + dir.foreach( '/mnt', function(path,stat,state,name,kind,ino,off,attr,path) + if kind == unix.DT_DIR then + table.insert( mountables, name ) + end + end) + if #mountables > 0 then + plugin.info = plugin.info .. '

mountables found:

' + foreach(mountables, function(k,mount) + if mount ~= '..' then + plugin.info = plugin.info .. '/mnt/' .. mount .. ' ' + end + end) + end +end --- this registers the plugin UI -local plugin = { - name = 'Cloud drives disabled', - info = '', - opts = { - { - key='app.opts.plugin_clouddrive_remote_1', - title='Cloud 1', - info='rclone remotes show up in the dropdown (if installed)', - default="-- disabled --", - values=remotes - }, - { - key='app.opts.plugin_clouddrive_remote_1_target', - title='Cloud 1 URL', - info='at which URL should this folder show up?', - default=app.opts.plugin_diskscan_dir:sub(2) .. "/cloud1", - values='' - }, - { - key='app.opts.plugin_clouddrive_remote_2', - title='Cloud 2', - info='rclone remotes show up in the dropdown (if installed)', - default="-- disabled --", - values=remotes - }, - { - key='app.opts.plugin_clouddrive_remote_2_target', - title='Cloud 2 URL', - info='at which URL should this folder show up?', - default=app.opts.plugin_diskscan_dir:sub(2) .. "/cloud2", - values='' - }, - { - key='app.opts.plugin_clouddrive_remote_3', - title='Cloud 3', - info='rclone remotes show up in the dropdown (if installed)', - default="-- disabled --", - values=remotes - }, - { - key='app.opts.plugin_clouddrive_remote_3_target', - title='Cloud 3 URL', - info='at which URL should this folder show up?', - default=app.opts.plugin_diskscan_dir:sub(2) .. "/cloud3", - values='' - }, - } -} +function scanMounts(mounts) + dir.foreach( app.opts.plugin_diskscan_dir, function(path,stat,state,name,kind,ino,off,attr,fullpath) + local stdout = util.execute("mountpoint " .. fullpath) + if stdout:match("is a mountpoint") then + local mount = {to=fullpath} + mount.from = util.execute('readlink '.. fullpath) + table.insert( mounts, mount ) + end + end) + if #mounts > 0 then + plugin.info = plugin.info .. '
active mounts found:

' + foreach(mounts, function(k,mount) + if mount ~= '..' then + plugin.info = plugin.info .. ''.. mount.from .. ' to URL ' .. mount.to .. ' ' + end + end) + end +end + +function scanBackends(backends) + local stdout, retstr, retcode = util.execute("rclone help backends 2>&1") + local lines = util.split(stdout,"\n") + plugin.info = plugin.info .. app.tpl([[ +

+
+      ${stdout}
+    
+ ]], { stdout = stdout}) +end + +-- scan rclone remotes & mountpoints +local mounts = {} +local mountables = {} +local remotes = {} +local backends = {} +scanMounts(mounts) +scanMountables(mountables) + +if util.cmdExist('rclone') then + scanRemotes(remotes) + scanBackends(backends) +else + plugin.info = plugin.info .. '

No (rclone) remotes available. Please make sure to install/configure remotes via rclone, rclone-ui or rclone-browser
' +end + +if not util.cmdExist('rs-mount') then + plugin.info = plugin.info .. '
RemoteStorage cannot be mounted, as rs-mount is not installed (yet).

' +end return plugin diff --git a/src/plugin/diskscan.lua b/src/plugin/diskscan.lua index c6b91f5..219962b 100644 --- a/src/plugin/diskscan.lua +++ b/src/plugin/diskscan.lua @@ -107,11 +107,13 @@ return { -- check for directory thumbnail sidecarfile local thumbExt = {"png","jpg","webp"} foreach( thumbExt, function(k,ext) - local thumbnailFile = fullpath .. '.' .. ext - if util.fileExist( thumbnailFile ) then - experience.thumbnail = "/" .. util.stripPath(thumbnailFile) - print("✅ └─ plugin/diskscan: detected .".. ext:lower() .. " thumbnail file: " .. thumbnailFile) - end + local files = { fullpath .. '.' .. ext, fullpath .. '/' .. name .. '.' .. ext } + foreach( files, function(kk,thumbnailFile) + if util.fileExist( thumbnailFile ) then + experience.thumbnail = "/" .. util.stripPath(thumbnailFile) + print("✅ └─ plugin/diskscan: detected .".. ext:lower() .. " thumbnail file: " .. thumbnailFile) + end + end) end) -- scan children me.scanDir(fullpath,experience,seen,symlinks) diff --git a/src/plugin/federation.lua b/src/plugin/federation.lua index 8ae1413..7971936 100644 --- a/src/plugin/federation.lua +++ b/src/plugin/federation.lua @@ -11,20 +11,80 @@ app.on('set', function(k,v) print("content was updated") end) +--app.on('dir', function(opts) +-- local me = app.plugin.federation +-- if opts.urlpath == '/' then +-- foreach( me.portals, function(k,portal) +-- print("JAAA") +-- table.insert( opts.data.items,{ +-- url = portal.url, +-- name = portal.name, +-- thumbnail = portal.thumbnail +-- }) +-- end) +-- end +--end) --- this registers the plugin UI local plugin = { - name = 'Federation', - info = '', + name = 'Federation disabled', + info = 'updating values requires a xrforge restart', opts = { -- TODO populate this based on xrforge-federation.json { - key='app.opts.plugin_xrforges_xrforge_isvery_ninja', - title='xrforge.isvery.ninja', - info='', - default="enabled", - values={enabled='enabled',disabled='disabled'} + key='app.opts.plugin_federation_url_1', + title='Federation URL 1', + info="a json with (janus)xr(forge) URLs", + default="./federation.json", + values='' + }, + { + key='app.opts.plugin_federation_url_2', + title='Federation URL 1', + info="a json with (janus)xr(forge) URLs", + default="", + values='' + }, + { + key='app.opts.plugin_federation_url_3', + title='Federation URL 1', + info="a json with (janus)xr(forge) URLs", + default="", + values='' } } } +-- +-- PROBLEM: every request has its own app context +-- so the util.fetch() didn't help here +-- sqlite is needed to share state + +app.on('init', function() + local me = app.plugin.federation + me.portals = {} + local federations = {} + -- load previous federations if any + for k=1,3 do + if app.opts['plugin_federation_url_'..k] and #app.opts['plugin_federation_url_'..k] > 0 then + table.insert(federations, app.opts['plugin_federation_url_'..k]) + end + end + + foreach(federations, function(k,url) + if url:match('^[./]') then + url = util.getURL( url:gsub('^[./]',''), {server=true} ) + end + util.fetch(url, nil, function(req,res,next) + if req.body then + local t = DecodeJson(req.body) + if t then + foreach( t, function(k,opt) + table.insert( me.portals, opts ) + end) + end + end + end) + end) +end) + +-- this registers the plugin UI return plugin diff --git a/src/plugin/moderation.lua b/src/plugin/moderation.lua index 7decea6..087541a 100644 --- a/src/plugin/moderation.lua +++ b/src/plugin/moderation.lua @@ -23,7 +23,23 @@ local plugin = { default = "wip beta -v[0-9] customerX", values='' }, - } + }, + isAllowed = function(exp) + local me = app.plugin.moderation + local allowed = true + if not me.keywords then + me.keywords = util.split(app.opts.plugin_moderation_mutekeywords," ") + end + foreach( me.keywords, function(k,pattern) + if exp.name:match(pattern) + or exp.title:match(pattern) + or exp.url:match(pattern) + then + allowed = false + end + end) + return allowed + end } return plugin diff --git a/util/xrforge.sh b/src/util/xrforge.sh similarity index 100% rename from util/xrforge.sh rename to src/util/xrforge.sh