299 lines
11 KiB
Bash
Executable file
299 lines
11 KiB
Bash
Executable file
#!/bin/sh
|
|
oci=$(which podman || which docker)
|
|
test -n "$APPNAME" || export APPNAME=XRForge
|
|
test -n "$TAGLINE" || export TAGLINE="Publish single-file XR experiences to immersive networks"
|
|
test -n "$UPLOAD_PATH" || export UPLOAD_PATH=/mnt/experiences
|
|
test -n "$THEME" || export THEME=slate
|
|
test -n "$HOMEPAGE" || export HOMEPAGE=/models
|
|
test -n "$GODOT_VERSION" || export GODOT_VERSION=4.4.1-stable
|
|
test -n "$CORS_PROXY" || export CORS_PROXY=1
|
|
db=/config/manyfold.sqlite3
|
|
|
|
# utility funcs
|
|
error(){ echocolor "[error]" "$*"; exit 1; }
|
|
echocolor(){ printf "\033[96m%s\033[0m \033[95m%s\033[0m %s\n" "$1" "$2" "$3"; }
|
|
debug(){ set -x; "$@"; set +x; }
|
|
|
|
create_config(){
|
|
test -d ./config || mkdir config
|
|
test -d ./experiences || mkdir experiences
|
|
}
|
|
|
|
run(){
|
|
{
|
|
test -x ${oci} || { echo "warning: not running manyfold OCI container [install podman/docker/etc]"; exit; }
|
|
${oci} rm -f xrforge
|
|
} 1>&2
|
|
#-e NO_OVERLAYFS=true \
|
|
#-e NO_DEFAULTDB=true \
|
|
#-e PUBLIC_HOSTNAME=localhost \
|
|
#-e PUBLIC_PORT=80 \
|
|
echo ${oci} run "$@" -p 8790:3214 -p 8791:3215 --name xrforge \
|
|
-e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l \
|
|
-e DATABASE_ADAPTER=sqlite3 \
|
|
-e FEDERATE_DRIVE_HOST=http://localhost:8791 \
|
|
-e SUDO_RUN_UNSAFELY=enabled \
|
|
-e MULTIUSER=enabled \
|
|
-e FEDERATION=enabled \
|
|
-e THEME=$THEME \
|
|
-e HOMEPAGE=$HOMEPAGE \
|
|
-e GODOT_VERSION=4.4.1-stable \
|
|
-e FEDERATE_DRIVE_CACHE=5s \
|
|
--cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
|
|
xrforge
|
|
#ghcr.io/manyfold3d/manyfold-solo:latest
|
|
}
|
|
|
|
overlayfs(){
|
|
test -d /manyfold || return 0; # nothing to override
|
|
echocolor "[$APPNAME]" "applying filesystem overlay"
|
|
cd /manyfold
|
|
rsync -rvzi * /.
|
|
}
|
|
|
|
# cron-like function using sleep (./manifold.sh infinite 3600 zip -r /backup.zip /)
|
|
infinite(){
|
|
trap 'echocolor "/bin/infinite $*: process ended..infinite does not care.."; sleep 2s' INT
|
|
interval=$1
|
|
shift
|
|
loop(){
|
|
echocolor "[/bin/infinite $*]" "$(date) started"
|
|
while sleep ${interval}; do
|
|
echocolor "[/bin/infinite $*]" "$(date) executing"
|
|
"$@"
|
|
done;
|
|
}
|
|
loop "$@"
|
|
}
|
|
|
|
# flexible unixy hook-mechanism which executes all files in ~/hook.d/foo/* when
|
|
# calling 'hook foo bar'
|
|
hook(){
|
|
test -z "$1" && { echo "usage: hook <cmd_or_jsfunction> [args]"; return 0; }
|
|
logger "$ hook $*"
|
|
cmd=$1
|
|
shift
|
|
test -d ~/hook.d/$cmd && {
|
|
find -L ~/hook.d/$cmd/ -type f -executable -maxdepth 1 | sort -V | while read hook; do
|
|
logger " |+ hook $hook $*"
|
|
{ $hook "$@" || true; } 2>&1 | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1' | logger
|
|
done
|
|
}
|
|
}
|
|
|
|
start_hook_daemon(){
|
|
# every day call scripts in ~/hook.d/cleanup_daily/*
|
|
# 86400 secs = 1 day 3600 = 1 hour
|
|
$0 infinite 86400 hook daily &
|
|
$0 infinite 3600 hook hourly &
|
|
# trigger hooks when files change in /mnt
|
|
#find /mnt -type d -mindepth 1 -maxdepth 1 | while read dir; do
|
|
# echocolor "[$APPNAME]" "listening to inotify events in $dir"
|
|
# # scan for '/mnt/experiences/creatorname/#234/ MODIFY foo.glb' e.g.
|
|
# # scan for '/mnt/experiences/creatorname/#234/ MOVED_TO foo.glb' e.g.
|
|
# inotifywait -r -m $dir | awk '/.*/ { print $0 }; $2 ~ /(CREATE|MODIFY|MOVED_TO|DELETE)/ && $3 ~ /datapackage/ { system("'$0' hook datapackage_"$2" "$1""$3) }' &
|
|
#done
|
|
|
|
## force-trigger processing hooks in /mnt
|
|
#find /mnt | grep datapackage | xargs -n1 $0 hook inotify_MODIFY
|
|
}
|
|
|
|
|
|
db(){
|
|
default(){
|
|
test -f /manyfold/manyfold.sql && ! test -f /config/manyfold.sql && {
|
|
echocolor "[$APPNAME]" "copying default database"
|
|
test -d /config || mkdir /config
|
|
cat /manyfold/manyfold.sql | sqlite3 $db
|
|
}
|
|
}
|
|
dump(){
|
|
sqlite3 source.db ".dump" > /manyfold/manyfold.sql
|
|
}
|
|
import(){
|
|
sqlite3 $db < "$1" #/manyfold/manyfold.sql
|
|
}
|
|
"$@"
|
|
}
|
|
|
|
add_lib_to_db(){
|
|
name=$(basename $1)
|
|
sqlite3 $db "INSERT INTO libraries SELECT NULL, '$1', DATE('NOW'), DATE('NOW'), '', '', '$name', NULL, '', 'filesystem', '', '', '', '', '', '$name', 1 WHERE NOT EXISTS (SELECT 1 FROM libraries WHERE path = '$1');"
|
|
}
|
|
|
|
set_upload_path(){
|
|
echocolor "[$APPNAME]" "configuring upload library"
|
|
test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH
|
|
add_lib_to_db $UPLOAD_PATH
|
|
id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';")
|
|
sqlite3 $db "INSERT INTO settings VALUES(6,'default_library',replace('--- $id\n','\n',char(10);
|
|
INSERT INTO sqlite_sequence VALUES('settings',6);"
|
|
}
|
|
|
|
mount_dir(){
|
|
find /mnt -type d -mindepth 1 -maxdepth 1 | while read dir; do
|
|
echocolor "[$APPNAME]" "mounting $dir as library"
|
|
add_lib_to_db "$dir"
|
|
done
|
|
}
|
|
|
|
mount_rclone(){
|
|
|
|
libraries(){
|
|
rclone listremotes | while read remote; do
|
|
dir="${remote/:/}"
|
|
test -d /mnt/$dir || mkdir /mnt/$dir
|
|
echocolor "[$APPNAME]" "rclone: mounting $remote to /mnt/$dir"
|
|
debug rclone mount --daemon $remote /mnt/$dir -vv
|
|
add_lib_to_db /mnt/$dir
|
|
done
|
|
}
|
|
|
|
library(){
|
|
echocolor "[$APPNAME]" "rclone: mounting $RCLONE_REMOTE to /mnt/$RCLONE_REMOTE"
|
|
test -d /mnt/$RCLONE_REMOTE || mkdir /mnt/$RCLONE_REMOTE
|
|
debug rclone mount --daemon ${RCLONE_REMOTE}: /mnt/$RCLONE_REMOTE -vv
|
|
add_lib_to_db /mnt/$RCLONE_REMOTE
|
|
}
|
|
|
|
test -n "$RCLONE_REMOTE" && library
|
|
test -n "$RCLONE_REMOTE" || libraries
|
|
}
|
|
|
|
set_global(){
|
|
echocolor "[$APPNAME]" "setting $1 to '$2'"
|
|
debug sqlite3 /config/manyfold.sqlite3 'CREATE TABLE IF NOT EXISTS "settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "var" varchar NOT NULL, "value" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);'
|
|
debug sqlite3 /config/manyfold.sqlite3 "
|
|
INSERT OR REPLACE INTO settings (id, var, value, created_at, updated_at)
|
|
VALUES (
|
|
(SELECT id FROM settings WHERE var = '$1'),
|
|
'$1',
|
|
$2,
|
|
COALESCE((SELECT created_at FROM settings WHERE var = '$1'), datetime('now')),
|
|
datetime('now')
|
|
);
|
|
"
|
|
}
|
|
|
|
set_admin(){
|
|
echocolor "[$APPNAME]" "adding xrforge admin"
|
|
read -r -d '' QUERY <<EOF
|
|
INSERT INTO users VALUES(1,'xrforge@localhost','\$2a\$12\$u/j8LRzbPiJRHmi1eV/fvOXXiKxN2vBGtNd.Pt28w.wOnq3rnfpzO','2025-07-25 10:52:56.989975','2025-07-25 12:46:27.338917','xrforge','{"models":true,"creators":true,"collections":true,"per_page":12}','{"grid_width":200,"grid_depth":200,"show_grid":true,"enable_pan_zoom":false,"background_colour":"#000000","object_colour":"#ffffff","render_style":"original"}','{"threshold":2,"heatmap":true,"keypair":true,"sorting":"frequency"}','{"missing":"danger","empty":"info","nesting":"warning","inefficient":"info","duplicate":"warning","no_image":"silent","no_3d_model":"silent","non_manifold":"warning","inside_out":"warning","no_license":"silent","no_links":"silent","no_creator":"silent","no_tags":"silent"}','{"hide_presupported_versions":true}',NULL,'2025-07-25 12:46:27.325287',NULL,NULL,0,NULL,NULL,NULL,NULL,'71863vkppj6k',1,1,1);
|
|
EOF
|
|
debug sqlite3 /config/manyfold.sqlite3 "$QUERY"
|
|
debug sqlite3 /config/manyfold.sqlite3 'INSERT INTO users_roles VALUES(1,1);'
|
|
#debug sqlite3 /config/manyfold.sqlite3 'INSERT INTO users_roles VALUES(1,4);'
|
|
#debug sqlite3 /config/manyfold.sqlite3 "INSERT INTO sqlite_sequence VALUES('users',1);"
|
|
}
|
|
|
|
set_homepage(){
|
|
test "$HOMEPAGE" = "/" && return 0 # nothing to do
|
|
echocolor "[$APPNAME]" "enforcing homepage path"
|
|
debug sed -i 's|root to:.*|root to: redirect("'$HOMEPAGE'")|g' /usr/src/app/config/routes.rb
|
|
}
|
|
|
|
rename_app(){
|
|
echocolor "[$APPNAME]" "renaming manyfold to $APPNAME"
|
|
sed -i 's/title: Manyfold/title: '$APPNAME'/g' /usr/src/app/config/locales/*.yml
|
|
sed -i 's|powered_by_html:.*|powered_by_html: Radically opensource-powered by <a href="https://manifold.app" target="_blank">Manyfold</a>, <a href="https://xrfragment.org">XR Fragments</a>, <a href="https://github.com/jbaicoianu/janusweb" target="_blank">JanusWeb</a> and <a href="https://nixos.org" target="_blank">NIX</a>|g' /usr/src/app/config/locales/*.yml
|
|
sed -i 's|Models|Experiences|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml
|
|
sed -i 's|Model|Experience|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml
|
|
}
|
|
|
|
start_syslog(){
|
|
touch /var/log/messages
|
|
syslogd -n & # start syslogd
|
|
echocolor started syslog | logger
|
|
tail -f /var/log/messages &
|
|
}
|
|
|
|
scan_libraries(){
|
|
cd /usr/src/app
|
|
echocolor "scanning libraries"
|
|
bin/manyfold libraries scan
|
|
}
|
|
|
|
scan_experience(){
|
|
id=$1
|
|
cd /usr/src/app
|
|
# don't do this when all libraries are already being scanned
|
|
rails_query 'Model.find('$id').add_new_files_later(include_all_subfolders:false)'
|
|
}
|
|
|
|
rails_query(){
|
|
cd /usr/src/app
|
|
echo "$*" | bin/rails console
|
|
}
|
|
|
|
force_public(){
|
|
test -n "$NO_PUBLIC_ONLY" && return 0 # nothing to do
|
|
echocolor "forcing public-only models"
|
|
infinite 60 rails_query 'Model.find_each { |it| it.grant_permission_to("view", nil) }' &
|
|
}
|
|
|
|
get_xrfragment_assets(){
|
|
test -n "$NO_ASSETS" && return 0 # nothing to do here
|
|
test -d /mnt/asset || {
|
|
echocolor "fetching XR Fragments asset & templates"
|
|
mkdir -p /mnt/asset/xrfragment /mnt/templates/xrfragment
|
|
cd /tmp
|
|
timeout 50 wget "https://codeberg.org/coderofsalvation/xrfragment/archive/main.zip"
|
|
unzip main.zip
|
|
cp -r xrfragment/assets/library /mnt/asset/xrfragment/\#1
|
|
cp -r xrfragment/assets/template /mnt/templates/xrfragment/\#2
|
|
}
|
|
add_lib_to_db /mnt/asset
|
|
add_lib_to_db /mnt/templates
|
|
}
|
|
|
|
init_database(){
|
|
test -f ${db}.xrforgeinit && exit 0 # already inited
|
|
sleep 3
|
|
test -z "$THEME" || set_global theme "'$THEME'"
|
|
set_admin
|
|
set_global site_name "'$APPNAME'"
|
|
set_global site_tagline "'$TAGLINE'"
|
|
set_global model_tags_auto_tag_new "replace('--- \"\"\\n','\\n',char(10))"
|
|
set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))"
|
|
#set_global about "$ABOUT"
|
|
set_upload_path &
|
|
get_xrfragment_assets
|
|
mount_dir
|
|
scan_libraries &
|
|
touch ${db}.xrforgeinit
|
|
}
|
|
|
|
# The new entrypoint of the docker
|
|
boot(){
|
|
echocolor "[$APPNAME]" "booting..."
|
|
test -z "$NO_OVERLAYFS" && overlayfs
|
|
start_syslog
|
|
rename_app
|
|
set_homepage
|
|
start_hook_daemon
|
|
mount_rclone
|
|
force_public &
|
|
|
|
# enable development mode (disables template caching etc)
|
|
test -n "$DEV" && {
|
|
# sed -i 's|^exec.*|exec s6-setuidgid $PUID:$PGID bin/dev|g' /usr/src/app/bin/docker-entrypoint.sh
|
|
sed -i 's|config.enable_reloading = false|config.enable_reloading = true|g' /usr/src/app/config/environments/production.rb
|
|
apk add vim
|
|
}
|
|
|
|
exec "$@" # exec prevents error 's6-overlay-suexec: fatal: can only run as pid 1'
|
|
}
|
|
|
|
is_inside_container(){
|
|
test -d /package/admin && return 0 || return 1
|
|
}
|
|
|
|
usage(){
|
|
echocolor "Usage:" manyfold.sh "<cmd>"
|
|
echocolor "Cmds:"
|
|
echocolor " " "run [-d] " "# runs a OCI container (needs podman/docker)"
|
|
exit 0
|
|
}
|
|
|
|
test -n "$1" && "$@"
|
|
test -n "$1" || usage
|