various bugfixes + clouddrive update

This commit is contained in:
Leon van Kammen 2026-08-03 19:19:07 +02:00
parent 50b0fbbba4
commit 7cd6df24c7
17 changed files with 366 additions and 117 deletions

4
.env
View file

@ -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/" && {

View file

@ -1,4 +1,65 @@
XRForge is a tiny, powerful tool to interconnect XR experiences.<br>
<br>
An open, seamless XR web built from simple URLs and directories, without gatekeepers.<br>
<br>
<br>
XRForge transforms standard local or remote file directories into interconnected 3D spaces.
<br>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 <cmd> [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

View file

@ -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

View file

@ -188,7 +188,7 @@ sb = {
header=GetHeaders(),
url=GetPath(),
protocol=GetScheme(),
body={}
body=GetBody(),
}
local res={
_status=nil,

View file

@ -75,10 +75,17 @@ util.templateFile = function(file,data)
return util.template( LoadAsset(file), data)
end
util.getURL = function()
local port = ':' .. GetPort()
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 .. GetPath()
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("/$","")

View file

@ -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

View file

@ -270,7 +270,11 @@ img.icon{ display:none; }
.info{
background:#224;
padding:10px;
padding:10px 20px;
overflow:clip;
}
.info .tag{
margin-bottom:7px;
}
b.tag{

4
src/federation.json Normal file
View file

@ -0,0 +1,4 @@
[
{"url":"https://xrforge.isvery.ninja", "name":"XRForge", "thumbnail":""},
{"url":"https://www.janusxr.org", "name":"JanusXR", "thumbnail":""}
]

View file

@ -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)
</td>
<td>
]]
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('<select name="${key}" value="${value}">\n', {
key = opt.key,
value = value
})
print(opt.key:gsub("^app.opts.","") )
foreach( opt.values, function(k,v)
html = html .. util.template('<option value="${k}">${v}</option>\n',{
k=k,
@ -30,21 +30,21 @@ function pluginInput(opt)
end)
html = html .. "</select>\n"
else
html = html .. util.template('<input type="text" name="${key}" value="${value}"></input>',{
html = html .. util.template('<input type="text" name="${key}" value="${value}" placeholder="${placeholder}"></input>',{
key = opt.key,
value = value
value = value,
placeholder= opt.placeholder or ''
})
end
html = html .. util.template([[
</td>
<td class="col3">${info}</td>
</td>
</tr>
]],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)

View file

@ -15,11 +15,21 @@
<td></td>
<td class="">
{if #plugin.info > 2 then
if plugin.infowide then
print([[
</td>
<tr><td colspan="3">
<div class="col3 info">
${plugin.info}
</div>
]])
else
print([[
<div class="col3 info">
${plugin.info}
</div>
]])
end
end}
<br/><br/>
</td>

View file

@ -21,6 +21,7 @@ return {
local matchpath = urlpath
if urlpath == '/' then matchpath = rootpath end -- generate janusxr footer for root url
foreach( app.cache.items, function(k,v)
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) .. " " ..
@ -43,7 +44,7 @@ return {
res.items[ v.id ] = v
res.parent = parent
end
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

View file

@ -7,9 +7,9 @@
end
end}
</assets>
<room pos="0 0 -4" skybox="true" showavatar="false" use_local_asset="room_plane">
<room pos="0 0 4" skybox="true" showavatar="false" use_local_asset="room_plane" zdir="0 0 -1">
<light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" />
<text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" rotation="-180 0 -180" emissive="0 0 0" roughness="0.3" metalness="0" />
<text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" emissive="0 0 0" roughness="0.3" metalness="0" />
{if item then
print('\t<object id="${url}"/>')
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<object js_id="portal_' .. i .. '" pos="' .. center .. ' 0 0" rotation="0 180 0">')
print('\t\t<object js_id="portal_' .. i .. '" pos="' .. center .. ' 0 0">')
print('\t\t <link url="' .. url .. '" thumb_id="img' .. i .. '" title_col="0.4 0.37 0.4" pos="0 0 0.001" draw_glow="true" scale="3 3 0.1" />')
print('\t\t <link url="' .. url .. '" shader_id="xrforgeportal" pos="0 0 0.003" draw_glow="false" scale="3 3 0.1" />')
print('\t\t <text text="' .. v.title .. '" pos="0 3.4 0" col="1 1 1" font_scale="false" scale="0.2 0.2 0.2" />')

View file

@ -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 .. '<br><br><a href="https://rclone.org" target="_blank">rclone</a> remotes detected:<br><br>'
foreach(remotes, function(k,remote)
plugin.info = plugin.info .. '<b class="tag">' .. remote .. ':/</b>&nbsp;'
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 .. '<br><br>mountables found:<br><br>'
foreach(mountables, function(k,mount)
if mount ~= '..' then
plugin.info = plugin.info .. '<b class="tag">/mnt/' .. mount .. '</b>&nbsp;'
end
end)
end
end
-- this registers the plugin UI
local plugin = {
name = 'Cloud drives <b class="tag">disabled</b>',
info = '',
opts = {
{
key='app.opts.plugin_clouddrive_remote_1',
title='Cloud 1',
info='<a href="https://rclone.org">rclone</a> 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='<a href="https://rclone.org">rclone</a> 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='<a href="https://rclone.org">rclone</a> 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 .. '<br>active mounts found:<br><br>'
foreach(mounts, function(k,mount)
if mount ~= '..' then
plugin.info = plugin.info .. '<b class="tag">'.. mount.from .. ' </b> to URL <b class="tag">' .. mount.to .. '</b>&nbsp;'
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([[
<br><br>
<pre>
${stdout}
</pre>
]], { 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 .. '<br><br>No (rclone) remotes available. Please make sure to install/configure remotes via <a href="https://rclone.org" target="_blank">rclone</a>, <a href="https://github.com/rclone-ui/rclone-ui" target="_blank">rclone-ui</a> or <a href="https://github.com/kapitainsky/RcloneBrowser" target="_blank">rclone-browser</a><br>'
end
if not util.cmdExist('rs-mount') then
plugin.info = plugin.info .. '<br><a href="https://remotestorage.io" target="_blank">RemoteStorage</a> cannot be mounted, as <a href="https://github.com/remotestorage/fuse" target="_blank">rs-mount</a> is not installed (yet).<br><br>'
end
return plugin

View file

@ -107,12 +107,14 @@ return {
-- check for directory thumbnail sidecarfile
local thumbExt = {"png","jpg","webp"}
foreach( thumbExt, function(k,ext)
local thumbnailFile = fullpath .. '.' .. ext
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)
end

View file

@ -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 <b class="tag">disabled</b>',
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

View file

@ -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