From 756d3b86ba648d21b77293bb571e23dff695a3a9 Mon Sep 17 00:00:00 2001 From: ToledoEM <8144940+ToledoEM@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:57:45 +0000 Subject: [PATCH] Add Manyfold add-on integration --- manyfold/CHANGELOG.md | 55 ++++ manyfold/Dockerfile | 27 ++ manyfold/README.md | 132 +++++++++ manyfold/apparmor.txt | 18 ++ manyfold/build.yaml | 4 + manyfold/config.yaml | 53 ++++ manyfold/icon.png | Bin 0 -> 30815 bytes manyfold/logo.png | Bin 0 -> 14131 bytes .../s6-rc.d/manyfold/dependencies.d/base | 0 .../etc/s6-overlay/s6-rc.d/manyfold/finish | 3 + .../etc/s6-overlay/s6-rc.d/manyfold/run | 3 + .../etc/s6-overlay/s6-rc.d/manyfold/type | 1 + .../s6-rc.d/user/contents.d/manyfold | 0 manyfold/run.sh | 253 ++++++++++++++++++ manyfold/translations/en.yaml | 40 +++ 15 files changed, 589 insertions(+) create mode 100644 manyfold/CHANGELOG.md create mode 100644 manyfold/Dockerfile create mode 100644 manyfold/README.md create mode 100644 manyfold/apparmor.txt create mode 100644 manyfold/build.yaml create mode 100644 manyfold/config.yaml create mode 100644 manyfold/icon.png create mode 100644 manyfold/logo.png create mode 100644 manyfold/rootfs/etc/s6-overlay/s6-rc.d/manyfold/dependencies.d/base create mode 100755 manyfold/rootfs/etc/s6-overlay/s6-rc.d/manyfold/finish create mode 100755 manyfold/rootfs/etc/s6-overlay/s6-rc.d/manyfold/run create mode 100644 manyfold/rootfs/etc/s6-overlay/s6-rc.d/manyfold/type create mode 100644 manyfold/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/manyfold create mode 100755 manyfold/run.sh create mode 100644 manyfold/translations/en.yaml diff --git a/manyfold/CHANGELOG.md b/manyfold/CHANGELOG.md new file mode 100644 index 000000000..c84fc5442 --- /dev/null +++ b/manyfold/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +## 1.0.3 + +- Added the add-on to this repository under the official add-on folder/slug name `manyfold`. +- Updated image namespace and repository metadata for this repository: + - `image: ghcr.io/alexbelgium/manyfold-{arch}` + - `url: https://github.com/alexbelgium/hassio-addons/tree/master/manyfold` +- Updated AppArmor profile name to `hassio-addons/manyfold`. + +## 1.0.2 + +- Added build metadata for Home Assistant CI compatibility: + - `manyfold/build.yaml` with multi-arch `build_from` entries + - image template wiring in `config.yaml` +- Switched Docker base wiring to Home Assistant add-on build conventions: + - `Dockerfile` now uses `ARG BUILD_FROM` and `FROM ${BUILD_FROM}` +- Updated add-on `url` metadata to this repository path. +- Updated repository README to remove obsolete `import_path` references. +- Added ShellCheck compatibility headers (`# shellcheck shell=bash`) to s6/entry scripts using `with-contenv`. +- Removed default-valued metadata keys (`apparmor`, `boot`, `ingress`, `stage`) to satisfy add-on linter rules. + +## 1.0.1 + +- New resource tuning options for smaller HAOS hosts: + - `web_concurrency` + - `rails_max_threads` + - `default_worker_concurrency` + - `performance_worker_concurrency` + - `max_file_upload_size` + - `max_file_extract_size` +- Baseline AppArmor support: + - `apparmor: true` in add-on metadata + - `manyfold/apparmor.txt` profile +- Removed `import_path` option and runtime wiring to reduce confusion (it was not a web import endpoint). +- Kept ingress disabled and documented direct access on port `3214`. +- Host media mappings (`/share`, `/media`) are writable to support writable library paths like `/media/manyfold/models`. +- Home Assistant ingress/panel 404 issue by moving to direct web UI access model. +- Startup/runtime setup improvements: + - Better path validation for configured library and thumbnails paths + - Clearer startup logs and configuration summary + - More robust secret/bootstrap handling and ownership setup +- Recommended small-server baseline (see README): + - `web_concurrency: 1` + - `rails_max_threads: 5` + - `default_worker_concurrency: 2` + - `performance_worker_concurrency: 1` + +## 1.0.0 + +- First Home Assistant add-on packaging for Manyfold (`manyfold`). +- Runs `ghcr.io/manyfold3d/manyfold-solo` with persistent data under `/config`. +- Sidebar/web UI integration on port `3214`. +- Configurable storage paths and startup path safety checks. +- Non-root runtime defaults (`puid`/`pgid`) and startup ownership handling. diff --git a/manyfold/Dockerfile b/manyfold/Dockerfile new file mode 100644 index 000000000..d97c8a354 --- /dev/null +++ b/manyfold/Dockerfile @@ -0,0 +1,27 @@ +ARG BUILD_FROM=ghcr.io/manyfold3d/manyfold-solo:latest +FROM ${BUILD_FROM} + +# hadolint ignore=DL3041 +RUN set -eux; \ + if command -v apk >/dev/null 2>&1; then \ + apk add --no-cache bash coreutils jq openssl; \ + elif command -v apt-get >/dev/null 2>&1; then \ + apt-get update; \ + apt-get install -y --no-install-recommends bash coreutils jq openssl ca-certificates; \ + rm -rf /var/lib/apt/lists/*; \ + elif command -v dnf >/dev/null 2>&1; then \ + dnf install -y bash coreutils jq openssl; \ + dnf clean all; \ + else \ + echo "Unsupported base image: missing apk/apt-get/dnf"; \ + exit 1; \ + fi + +COPY run.sh /run.sh +COPY rootfs / + +RUN chmod +x /run.sh \ + && chmod +x /etc/s6-overlay/s6-rc.d/manyfold/run \ + && chmod +x /etc/s6-overlay/s6-rc.d/manyfold/finish + +ENTRYPOINT ["/init"] diff --git a/manyfold/README.md b/manyfold/README.md new file mode 100644 index 000000000..98354845c --- /dev/null +++ b/manyfold/README.md @@ -0,0 +1,132 @@ +# Manyfold Home Assistant Add-on + +This add-on wraps `ghcr.io/manyfold3d/manyfold-solo` for Home Assistant OS with persistent storage and configurable host-backed media paths. + +Documentation: [manyfold.app/get-started](https://manyfold.app/get-started/) + +## Features + +- Runs Manyfold on port `3214`. +- Persists app data, database, cache, and settings under `/config` (`addon_config`). +- Uses a configurable library path on Home Assistant host storage. +- Refuses startup if configured paths resolve outside `/share`, `/media`, or `/config`. +- No external PostgreSQL or Redis required. +- Supports `amd64` and `aarch64`. +- Includes a baseline AppArmor profile. + +## Default paths + +- Library path: `/share/manyfold/models` +- Thumbnails path: `/config/thumbnails` + +## Installation + +1. In Home Assistant OS Add-on Store, open menu (`...`) -> `Repositories`. +2. Add the Git repository URL for this add-on repository root (the repo includes `repository.yaml` and `manyfold/`). +3. Refresh Add-on Store and install **Manyfold**. +4. Configure options (defaults are safe for first run): + - `library_path`: `/share/manyfold/models` + - `secret_key_base`: leave blank to auto-generate + - `puid` / `pgid`: set to a non-root UID/GID (see "Fix root warning (PUID/PGID)" below) + - optionally tune worker/thread and upload limits in "Small server tuning" below +5. Start the add-on. +6. Open `http://:3214`. + +Before first start, ensure your library folder exists on the host: + +```bash +mkdir -p /share/manyfold/models +``` + +Local development alternative on the HA host: + +1. Copy `manyfold/` to `/addons/manyfold`. +2. In Add-on Store menu (`...`), click `Check for updates`. +3. Install and run **Manyfold** from local add-ons. + +## Library/index workflow + +1. Drop STL/3MF/etc into `/share/manyfold/models` on the host. +2. In Manyfold UI, configure a library that points to the same container path. +3. Thumbnails and indexing artifacts persist in `/config/thumbnails`. + +## Options + +- `secret_key_base`: App secret. Auto-generated and persisted at `/config/secret_key_base` when empty. +- `puid` / `pgid`: Ownership applied to writable mapped directories (`/config` paths). +- `multiuser`: Toggle Manyfold multiuser mode. +- `library_path`: Scanned/indexed path. +- `thumbnails_path`: Persistent thumbnails/index artifacts (must be under `/config`). +- `log_level`: `info`, `debug`, `warn`, `error`. +- `web_concurrency`: Puma worker process count. +- `rails_max_threads`: Max threads per Puma worker. +- `default_worker_concurrency`: Sidekiq default queue concurrency. +- `performance_worker_concurrency`: Sidekiq performance queue concurrency. +- `max_file_upload_size`: Max uploaded archive size in bytes. +- `max_file_extract_size`: Max extracted archive size in bytes. + +## Small server tuning + +For low-memory HAOS hosts, start with: + +```yaml +web_concurrency: 1 +rails_max_threads: 5 +default_worker_concurrency: 2 +performance_worker_concurrency: 1 +max_file_upload_size: 268435456 +max_file_extract_size: 536870912 +``` + +Then restart the add-on and increase gradually only if needed. + +## Fix root warning (PUID/PGID) + +If Manyfold shows: + +`Manyfold is running as root, which is a security risk.` + +set `puid` and `pgid` in the add-on Configuration tab to a non-root UID/GID. + +Example: + +```yaml +puid: 1000 +pgid: 1000 +``` + +How to find the correct values in Home Assistant: + +1. Open the **Terminal & SSH** add-on (or SSH into the HA host). +2. If you know the target Linux user name, run: + +```bash +id +``` + +Use the `uid=` value for `puid` and `gid=` value for `pgid`. + +If you do not have a specific username, use the owner of the Manyfold folders: + +```bash +stat -c '%u %g' /share/manyfold/models +``` + +Set `puid`/`pgid` to those numbers. + +After changing values: + +1. Save add-on Configuration. +2. Restart the Manyfold add-on. +3. Check logs for `puid:pgid=:` and confirm the warning is gone. + +## Validation behavior + +- Startup fails if `library_path` or `thumbnails_path` resolve outside mapped storage roots. +- `thumbnails_path` must resolve under `/config` to guarantee persistence. +- Startup fails if `library_path` is not readable. + +## Notes + +- This baseline avoids Home Assistant ingress and keeps direct port access. +- If `puid`/`pgid` change, restart the add-on to re-apply ownership to mapped directories. diff --git a/manyfold/apparmor.txt b/manyfold/apparmor.txt new file mode 100644 index 000000000..91fcc4abe --- /dev/null +++ b/manyfold/apparmor.txt @@ -0,0 +1,18 @@ +#include + +profile hassio-addons/manyfold flags=(attach_disconnected,mediate_deleted) { + #include + #include + #include + #include + + # Baseline profile for Manyfold in HAOS. Keep broad compatibility while + # denying known high-risk kernel interfaces. + file, + network, + capability, + + deny /proc/kcore rwklx, + deny /proc/sysrq-trigger rwklx, + deny /sys/firmware/** rwklx, +} diff --git a/manyfold/build.yaml b/manyfold/build.yaml new file mode 100644 index 000000000..adc465afa --- /dev/null +++ b/manyfold/build.yaml @@ -0,0 +1,4 @@ +--- +build_from: + aarch64: ghcr.io/manyfold3d/manyfold-solo:latest + amd64: ghcr.io/manyfold3d/manyfold-solo:latest diff --git a/manyfold/config.yaml b/manyfold/config.yaml new file mode 100644 index 000000000..cf40dc0ca --- /dev/null +++ b/manyfold/config.yaml @@ -0,0 +1,53 @@ +name: "Manyfold" +slug: manyfold +description: "Manyfold 3D model manager as a Home Assistant add-on, using the upstream image with configurable library/index paths." +version: "1.0.3" +url: "https://github.com/alexbelgium/hassio-addons/tree/master/manyfold" +image: ghcr.io/alexbelgium/manyfold-{arch} +arch: + - amd64 + - aarch64 +startup: services +init: false + +ports: + 3214/tcp: 3214 +ports_description: + 3214/tcp: "Manyfold Web UI" + +webui: "http://[HOST]:[PORT:3214]" + +map: + - addon_config:rw + - share:rw + - media:rw + +options: + secret_key_base: "" + puid: 1000 + pgid: 1000 + multiuser: true + library_path: "/share/manyfold/models" + thumbnails_path: "/config/thumbnails" + log_level: "info" + web_concurrency: 4 + rails_max_threads: 16 + default_worker_concurrency: 4 + performance_worker_concurrency: 1 + max_file_upload_size: 1073741824 + max_file_extract_size: 1073741824 + +schema: + secret_key_base: str + puid: int + pgid: int + multiuser: bool + library_path: str + thumbnails_path: str + log_level: list(info|debug|warn|error) + web_concurrency: int + rails_max_threads: int + default_worker_concurrency: int + performance_worker_concurrency: int + max_file_upload_size: int + max_file_extract_size: int diff --git a/manyfold/icon.png b/manyfold/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bdc50ecbc252ba84c5e6ccc288db6e5218aa46f5 GIT binary patch literal 30815 zcmY(p1y~%xlLxxHxVzf|2_D>KS=`;--Q8i4pg{w{-AMuj2(BTx1$TG%;PCRlyLb0q z&(~AcHNUFr>YnNOs%K(Um1QtcNl^g+0EV2br20P!`%gti{CDn!n2r1+Ko4~paX|Gr z`Qg7tgq5zGwUQEm=^u{_Kmg(b;Qj;ocL0E-0EGYI0RVX*+5htDK!*RpfB=9fI{^Iu zVD$gd{}kDO^`G|tP`Co%|5+>m{Vy6;0QZ0Vf7IF<>^%Pw6jxb24*&oS?>`j?$jTx9 zXHCyeQ`b{hNm0ZP+*j1O(XFIoUWlKm0>{@bGi?H1qx7>_PkAM*d$rl2#rT?sl%8b}r7~ z|JXG%ck%KRp`rPYqyH`c-A_+D>;LoQ?D0Qg{R@!oKUdf|SlQYB*Y-bE;r~blRov~Y z{(1h7eF%r}e^ zz=n>)FYgAt3haK^NU|meoPPYMiyT|R@Z-IFdD}$J!+gk|(jN7_{k24W#Xfgp)>l~& zJZeSNO4Hz{=ucjq=UD-gn!p0E-Hx8xnX|LCv$ao`_v*JR6Vcc8J-gcqP4!ZBgA!k* z;Vp=tPGx@(Bz;2h&i_G}sE+$^m#b!bf4c5ddP|=Cu-6ke6|)}=_5QWHo=a!kz;1|C z(5@3iCWh~G=caSs8hMJ;mxtxUhhWOXuM@)#_K%UAEx}j!E||(gxNKPBS&V4zK5$+2ax%yhU9_V!VURCF)YNzni zz8W`{fX@ z_~_l&`9`p5BNQ|Y+}3)h3+~3f@5Jw}aRp{;slViRjfWEe7+NF&KgcDI8LI_kAg?Br zyVsoG+Y_sI9Hkx%8(CUXM3?l-YEli99x>(| zY7mDE3V%S70}hTNlG7;k6+L{ASsZ~{pH;Mpk}bWPbsEUp&1#mq?f_YSN6)aNhHyed z&;^CPzxeP-cX6%YE|D&)#tree9EjbYq^=&hxytj%9I$zPC<{!T&^`~UYfp4=_Goz? zeT=;vtce>Q=KQ+5Eq0lI{@7?!#)j0wpqex6k8tvD2B7hjB)$lM^-*yWevx3?`k%Z|8K>N0|htuIN`@i0xi+@F2mi@*PVOZ-J0t1|OBmSGo%d zut`Rum4rv1OIh|8GH3tV>aETb)e;#}IOyE$O|!uVs`-|(9D)6WV_SQ*HJvYaZl@xQ z92IfY<1uk>Qua}kVst-78-2$S4ij8>VA$J==9Gi8jw2388U?A0%mioxiFK?d#JAab zAtdaL4t^#H6Yc7%Onx4axa-ahMD4G(EXgz`<+y4f>ezj>omxo1`BRl0Z@%k94-Yx` zuA3;3>q3bw;P7W!KmzI2PcKQ(Vb9fdJ%dbZ$Zml9qE*ny6OEIaie8siSLQlB<;YDl zSLwYU6;C!}HQdD{;S$y2^F$BZc;*Umj$T!kVd*TUr)mz$ZZeSYClLVCRPu|Vj&nK+ zLJI!*;Aog|D8Ir6>+!IiA9@R&!;BRfyfsJTwS3QBu@gewj{T5He0$4yV29$!ONI6k z+Pgs-SXFxW-BN<@cz5IRuGa(0l%py~ccxaR{?3QvLB=xHqcOiLAw`opwX)2HcH_N7 z=C|#O7MXvKpn*~%5Ua0I_Lj7}|Ea2B)d7)1i9~YZnW;YP!@!0$Y7OE1WhBgjQug{O zeR*U0YP`xZfA@N^9PO_}mVm;Sykm~=pda6$@j|ck^mf4KI;Bqm4FF~8l)a`D`eA>< zi>J8xrU1!dtT3bE{CNGWStT|J8<3>31w1cea!0_u43(1)ySR|bjMp}2^3w1Vt8Db17ObY)4gQD z?x752j|-%+r9=@FYt0F5btK~vWbNeo7Gay}G~_P^7E88fmI{09!njxOe3K*;N-@a^ zaPr{9eQ*rLcXLu6A+##`HoteR*O~B@?`*r@Fs^QI=%LtCx#>E&FLd)qH`2m2;-`fC zeZTr-+%CPi`di=*YIj~@euu69UQNx}b^&-r z-)pCeqOtNrKkeE>l0U@7NG+;jOMw)^8fJ{zBij)!Ds4jHfnDryI)HXSR{RLyRq(>e zJ_a7pt$cCK){2umJddfQr?L}AF145z=%$z2lwhcmn&41F^Ff)}>oXrr2|>%sEc{9E zw)bfNbxt36^B^rW#=C6Sih0pAKRwDtZdwhnB>#G+`evF_rQ=iS%_XaaU5bjUc_tRt z!>)yoVR0n&P^FBZi8xLsUo=)EqFxvUzB7*A`SD|vi6-?&CB zgOU&5lxDY!`|4?l92pMO|Ld&$9bUz=a|Y>)T9ua=KF-D#@2tN=<4kuEtlrHT zt|hbg6{2s{&;V8HR!nHbK9S!d)ZyF2*)W4ioe0SoVG7Vbf;WN*P;+0sl)fHoe!5i) zePW19iQ9fMj?1lH$>Y29OEo_VFJI;jk5qZwqr1$ z>r^?*`CBPU7Y3lCYgvwBcm;0e^H;IfHp&u|hhs2w%$){k$I~ocIe`Ew@ZV7C@J>(df7Rr(0%gvr2j zZva#Pfj%9XbbwAIsL+_czjtlkqk+RAI&8g&Y-UL3oO{~QVX!9G9^Cs}map!Tv3G8Iz1KZ1?a7KQ=9B zgg%urfJRY`_$%!a0;d)oIs-ZNm}Aq1`8|Ro7}$YYvF8cJIZPh7jPCRx@&noq`bYhC z<6Bi5p4FSV`9%KZR^Xg9v}#u(^u4*H2g8?SI%6}uF_jiRNhnGQAk3w~Z5=g`%)G1) zZqEL-X^L>-dyOW)2c-s0h@Ohtod1=T9#L+c1t6r1gnq=u%3BnnqHv>f=hTIDR~V`Q z2o50g#u{-cB42}E?$7snivF3&F6&39RyR;d|7j-ZvRc0SU3)21$nm-jU?v%XLtZFt zbcDr}7#opUB=!Q!H>Zv#z{pBCKmm(07>LEVBM9mxw06Wh0N%;yg2QNWdeiIs`K`KZ z%4D=I^Zj&^!6xalNt?A{#HmALx?*9zCJdqGBp7Jm-)TSh8(e_FDEEe}=@V=ph6#4l zZ@(Pw@$ls9jAW3We}LuVga-nB87Ki&D`TdMwl1}vRzYQtjNSRC-Df_k7l0}9Dw8O@ z6W|{&hBho$O7WwMf5oz0sUqhtEkR zGu*4MJF!vro9rS}bldDBf2tnG0~iEBv9=kB`WV!N2Or1ZRi>`b<51Je-e^_rvf=mk zc@fepd@-3Z3)+jH+_C@2%#sXFyiO6~Z;uTvHMhOQ2EGYy5CMpidWtwVHU8>T%t1z$45SV58^Yee`|SskRE_=z8==vELNO?EM%etCsEl`BJ4+0U3_5#N785}{$NEi#J+F?%}7^t*ec;HzZP+iFJ~oUupp-PKKVBi-_YW>hGOVN!}zd)F7F(5 zP5ZG?Orl1qw^X`mG|Y1lEK9Cf0OPAu@&%Y*8Jd4J(E?BZNE%+^;aeBfTz@BNCdnv) zHw{c-^ai?So39N{$bqxx1&3|ier6eW7W1>M*#JZKekwbsUf24aK9I2RFNmP#brTIP zT@|i~eb?X){^ugK6)w+?G9gs zRLE|nTOGU4gtobbVHS>x)3rj%10AVLLSFph%pxJRdH$}sBhU!zs-I%g*gxiuEnnc? zeXXR5hR;Zw1$NK2G4KI0x4%s@f~C-iqPy#14R*plD@kO3dpL3@ZV|u8ViV&?G)RW! zteZXYzALEO0^^+<-Qrnd)&lyOYRB6B^OJkC*5Dz2fdS6`!G!Z=HD5Tea*f)VnPSa1 z6B-lL$BQ#6SH>FVdCnXE900us{hj=4W-=|3qeY>`sZg`L|eZsk@;qk8i5NK*dLA*0V)aMI_0(!APM) z2z!&zE6gU_V5|WJsX21lFwAh9u*gP2x%LYac;g2582D0v%6DN%GN*j~ph&33(5 z-x- zl=y7_-psdkewHtR#o^wV4v|tmZDTWzT2rdbr{-a_Hd(v@wLX9jFC&H3ZT};~vKf*5 z(%WVEqm2bS*gK3T==nSj%Gk2s5(&@rL0Jl7Ie=<$?O1#27D(7jDJvBOmvrdpkTbW5 z+{TcX4c;Hq5?VEE!YPr!Zj5KpRVeJu6_4;9gG@IJc5otyR#o!jyr!K1!J({63@WUBalWQSo7 zAKS?IRmZACM_q#H|g=tS2BCLbB%Fji1FZAX&tn059C?6iLlc+_0N^yVTn3& z5}CdKc@B*n(QjSK@4>JK_XywOFy0~kY`~22y{}C}TpsiE2*q@Z_qE63pb+A|U|n@F zG!-!wTd@r-pw*=}mzs7iRKI?cbR)HAN@U)-%vWJ?j8`_b7m2# z%ti^5`AT$A_b@ysU4?r8eN1!Jw&bNldC3sT#Ei6Q@D4}Q1nI!Q_V1~Jus*eVM?2dPR0wh{w!D2U#5;snn<_>hVkca$DCQi1C_5Y@O3b3iy(LnZ0lcveqG8& z+cU7oV?bA@iIT(MeOre#X}5^~{5|g94gTdc&--Q+8t$oCge`b395Q@XsV>2J@GlHm z*pQ!Fly7f#G~?09_BZLLoOpPWFtP)+Z(%?)$dv9*S1*gtU=I~I{Eqi-!WGn(`2p#= z&j=E57=->Y7y!h#d#CP!n3evCTSo5iL9eH^_?NFeCZ%2!&;C_hK$@Vz$aS;`{soiI zpwD8byL;rc6>1#T2HZXT?hN2j(HnKvMO;rv$U%;ggIv}R{ZFa>3;B_Bs1SwNAs8TA zi<63F&fbi{(Cmj+%Ax)f&@d*kRTOPyu-w_O^+3#&GuDlrCCFopiKCi1JGDft7OJIo zs&fgit#RFoFr_t}#QWVyWoi{73+N8;A@~OO8#MUihg|1CEPSdI3&xXyM0sV6OWD&H zv*D~Frs3BOsB$FOaq&&NZ!KGC+)Fq3wLSH2K$?mhmqKGEd38m?=u3MzC zx5?*KpPZk)q4giyTuLw@uRuUcGYm9dU|$ge*WCo@?!RPr$_2L-{^31T(sl3v7<|mN ziH%AzeZbT0oDtogD4ZE}=XK!)^7Ch8k=?uefy<$SRuqt@I9Tf42o2L4_VL`Lw6d-X zc}58RMni7$Hd1Md(k0~6#fpyUQR2xxomIDK%4$78Cc8OD{dkc8QBi{&-(yWZN|VwO zo@}F=gvFZwoXFUSaMu^yBeu;Wl#qO+X$)Ht_&CvRV^dR-_`F^Z)VaggbNri$$UdoP3v< zc<%~0pxb>2%gOryG?k)^@!3Sa;(dd!{^(vBwtf?M{y+v}`2>K=yXs z_6po3ZcRl_Ai};hfCRE~r+qR2XmKdrpO3#9TpkEGL`zJz388w#mfBmzv=HBY-~ zx*XeR^@TR_k{+Hvr4D~v5N9VoVgD*|zz{>ED>`_)(uc&UV3X+=KE-It80I(@qk7PL;B^c%e72J(_B?*31eLbnH7wv4Tifey}vYS5Q!(_pa&JZ^trS7XaZ#*YFB8`Pa6ri5w z6l}z+H6tqSc&c*BsAknQCtB=pJu`V_z%Rk@C?IELlRfbHP|;f#_T!wxw(^Nj#sq&2 zw>z0=V1cucPK4iL3abFO86jIUzwXB;uHT?JxW^vSM=`ROjmu*1*1l=qkHPGnPUZ4s zR8e?(t8JQHrr)ZgCH{Ozr$_z)rD8rnx~>|g>6Qp3z_E>1O>BvDB~EVOCeR^}+T8U; z_8mqR>N5l*L3*$#Urr}F^i;ipj=Rl=?T7I;tvGOMf#b08wA-cYL8b5yp)_<9-BNJc zF=*N$3rf@B-|v+u*=t9Kbefu`{+?<3yGDC=eVj&0@bN&8lWsdFSD7Mo3_j-Z7S`a9DMuJ(>K_|%3YVI}kytm4j0wjGwn zO*^F48y{_L=hLtzbP8jD78e&STi@vNRsj*42b6vF315A=ZjuF86cOCQti)=>0Ei@4 zQ+;2XYTQI+(3RAnmme;^Vm-v~Rmxr4^Crji8n9ptICgvuh9Qf+!z5|aQ|imOCf!En z>@UGR^pI4=&Macs05Op45r=HL)M4XR8~;uYJ+)G^y!_Ta5uprut)pj8XL)U!`_!F@DOe^l1#J$?+&aGW59)_rVQVit)9!E95dnfD;cQ;a zzs%h%=NB$IEL}PjU;Jb{*U->A6mECVgMLC@ll<{{_oMYh^LMrN*Knuy!7STePj~k$ zXP5JWt0!)tuk7Ls>{c|;I~v1Sp~i%wbn*nU+R@q59+-nkby80BqH_=*&Mhg{2I*4% zu+R{_w0aa)k?WbL?i=JsK_h`suU$GpUSAnX;ed-OY}?H_&LFbNsyxCz;^qKjrq(!mbDd z2(sW%`=63rsDCM0Xy|2Mo`dQc0LkHS@b&;t0M&tcX>IH$#!l!>%9WLrdS_isd#kI~ zmFw21X*N!LeN$#xfO-I*5}<#lhtS!O+hi(x@Hq;LcN)&H+3P04z_1~l77;y4`3{`~ zYon=4oG`>IzcE3^?wZGubZ$j_C{Fc5yRBE40Q%gFa-JR6wMO;%WoJjnD)+CRtH%R= z3aVUil*_cQ@VaouYlm=j%ehTvm@Q5Q`Qp_G@H8v_N&=VCa zpf!vaF$YxFC}kH5S??`+R0XZn zvljMckm6E+aEcd$$>qE>Rt7aM(x*es^mnq@@THWc%p~de)fq|iU$<#VY$rhL!MBQxL=dPZk2%U7RXLJChi`wTQ z+64%Vio;K1vmHwz7{+Cj3v8DtYz@nJfn*T3Dk0KU>+g7^uTm`IvOI8+uGX|w}<(!Mw7Z;1t4G~KI$hdh!G?Gxm^t2reAM03&+4P0x#e(Oz{KKxSCt-J}%=;V_WAACUHd5^mj@OH`B(2i0=m(6Jcr(6H!?5qcJqwHmfUcii$$8lJR<3=- z%m-V<1<=cE-_ur^K|cl&%MMaHORG__DaJkH0X|K35o3r8s!M<)a(c9I?eJytlXK9@>do$@&f(T04MrKfAHX+R0&{bn zcv@}8ui*~6Pjw$NfecY@y<3v+gK-<9rs!`skAR?b|Bg0CX=kTWqomq+bUuJ(?9YPW z!a8J1kY^HL#TEviCly&nE|v?B&kIBL0gq)Q;_~qwRmQ2W{B|dWK3Mb%Qa5hF5haVA zq3K*0i~nUV%TTyz1)qf7qnQpcnmmOFkCJqhSgZ80sY=84pFW8hhx@Haw$>TLLxP7p%Dw#1f8(2eSU*>O7+!uJ{_f@G@%~+sq@>!&w`&0{ zjQySnpGpICz%zxcftQ~~rSrT=i1vuRbMH)9RAQuC+i9W+Sf>l#3W;(GsiFZS&_F!3TJPq# zobKMW5ytBtk1N)U@M5?|;!mg`T`Xuq2x!^DZ+wD9buiE=P_ZhbA$eDHNo>uDE&}arQVsr_S-LC)909iD@j5BCS!4Oh!7Jl+`M#&IH@#IYqC?BO zo>?j$_fGG5zl3BHLlF=}voSG+ZRN!Br_|KyhafPd=c@QKg8_K-vrOXDzl6pA_R)(J z45M!iDo)%5EZT5;=>~^)>C_~0*pnXS4ybG*zj%6klgOvv9)YidwE87V)OhdC&Zgps z*rq>6srd3eB08;y5jTK$2=}Ag88M3(pS$|%Ba470J*0=nd{&2AEavFnM2I zF{ri6eOK8JJ+lr*?Zp2LVC5p8*}yF0NrwgsVOM{^kvnOHb1eCMKaZh;cZiqA$3Uvm z&79iPuq`QKVz2h)PGa)91iQQOUK?G|%gyYA&S9S4(`JQ=61sM>$L&sAe{>R^2YfzN z5b)RR^wY$e*x~>Od|T5iiKd1X&5&CaHR01|$TzY=IM6)X(P7USHL_do_=ciEbmRx@ zi^b46xL7)xQ^Eq3Kh&hO3&PRtef7MhmisakJN^sw)Ki#jT}=9!;t_XCGqKK z?_Xhpk+o+OL~B%M<}8UMhIO%+c*x!S_9R0uCzg#br&cj1p^Yms#IA#hly5H+E$)MZ zzXbs20<<6RlIgS`%q`y>vxi6l5HZX|N~iMM{inkRT5gMlj^rj8)t6a6(Y>v^q`O3u z*{RPu5oTuVHQS>xcKXfGxJVbM*zy6=^5~6IHC{=~b3J30TRM@5^;|WO)Sl~9v`bKi z)uc=a@g-Ogo`YC%mp?@Jz)d?~;G6RC_uHGz>GQ(f(doF)Y|~ojf~)aAQ2*R>rB&^Y zaIoZ7E`1FgcoS}uBtLOJ4O_sPy1aD@CNh^h24783%LyPY~B?N*@V@fV=Kn~3Sy-T3e(<(T1G z4$VIp^Q?6xW-I8k2QI~!A8cEbh+g~DK>6CzGbu=UaH|Aw~*ViOh!pjsn~U%&+2`QhA4weHw!W zvq}3Fg3|*lZQ)aA9AB;LY6VkKB^Gs;<(giM1>1xNJW0*lXnSYR;zWrZ0kg(hb~Y3w zFR#-+lQTbVYZ8)b03mY<8i?GUdRIeb1Gc+dsXtMv(z!Fr{$_^Km{APsVY78TKkFRU zh(60b?mXO0vjxAYaJa?-u-4g1XEEqK_M*)M1%xDIDoS%tjp)*qB~*wvCz|_ywns*< zES{Em-ITN9nZqF$L{;*KDU2695=IyF)ry^nebyTgm{04~Yb7N4;eTd(EWz?Ie3ggX zz{zmeRgN|hA1&H?-!Keqfq$&8TVb3bO8_P*iQTWjn&btenRI`TD=A&kg^CDIh9gkH zJwn9~oN?~9Mxe)z^=l0iF`?;gGILT4+U}qoMXgzbI9^_2tcfuE_^IuQdqSE2DsIeY{)jeA}lj@cb0 zny${2W16j3X#iFw(dgF^r(xqAcaKdCFDck;&sSh+JdUyy+tsGKro(*5TmE7B`jg$` z&P(ESjpz%8WDg$pdu;HDTT-YGf5RL-Zo|F4T|jxZvdTnnSdtC@5Vz#MT`01_fgH^* z+HWO5ua&JO%D=W$+A3|!rk$Y$_>x7s2$dxei&MuV5p#?0ec2=`113!Ybn>R4K&B(x z3efukJ2jXPRORh7e_QpJ<~Ap1x~2)?4a6tK++XmxQlu-k&U_m4?+?=&$el}xJgT<7wB=#h;QR4nA? zkCk5*e%j&b7iZEX*39q4dohXjP4Hdk>Y}pH zX$LsTU=Id&$cqC+00i9e!G~7a81|;wDtN@|w#jw9KJy*cK%Pf#I(*huxB zzN>u#D_Nnq626Ap8s~LN#sq%(TMJD{R0~EHBO0=Z#LNuo@m;|oo?0P}u&^VW+-SJP zwj|z?v}Z5VI2-WgGdKXyE0Wv+0(1EhLiIoNn~2Ydy=xHHL}S%2mQGESjpp4AQ0ocz zbf*sOU`V`L^0>sqfdX(uutPtai@`_NxQ`T-k-2~5;_Uwit-Y%i~~&FZe9Mn1dquODZb z=-C_tM!lms#k31={O{xwNRZjXUzq)i)Q*Uw(QE9ozEh`vls{_H+(Xxcmv!c4W70O> zd6KPlR1*`%bWi-6b7FCIoRT|Tj7hC^e7~66Wg;`3Z*k6RYZ{OrZ2+6IC{r0*mF}a0 zr3YDWOf%>%@9iPVYrx^*n!N7tR_Gq7_f%~an5|*!I7abf2-_$_{-qNf_7>r9htDbt z58Ix9CU+H}a-X4iYa#vtUxGcZ98!Cj8Cr>WJ5!6&&&xtS8|MU38aju=QsceKKF;$_ zDSNO{IG$O|K_o2;f-Ep*n8t2;ZA2rf=cdwS*x(A(fA&_oGdSeJnT7^=`J^4~dyy+7 zn!8s{@FDo^;TJd@U6rRWbsQb!Q1l|Wp*jg~W%s{m9~Hr&cl~aK(abf95vEG6r-lw) zz(w8%hq>MA!*;Y3+i?Vl8)&*tf5bm*$Y-c&6~Yr9CYwEkan1^I2U&Wqd+Sz>+mj+% zdqx*8gJ)V%2^1%vqcTK3I4&tUE)nKcqa~Sss&HN6b=W~5LkMkChkOT{ohA zY)Z=zg9oDT*lxH@b0NLS?)PetOXS{IUgHdJv!7Ob-luS&RYy|f! z4Cdn(4i;K95F@eTe^WB)qd$KkKs^}S5L}y;Wy{!bEa-44P5dhw*-Ks)_bRotO(E)c zN8gjI%r>F!649LG#PwqM`H|SCBbk-XEmWaJ%S7QxDB#f2JB(3*3>>oTjEA08`HSEP zTKw^}%MaG9F7xQ#eaBpYs4-&j8}S=n{z5s#Xzt=8x=uA=6aYIIjCRoHB@J`DzE&+7 z*Hy>IA=%)3P(I=l6CQ%wOpDsTVq3HC47-(#yvHI$4x7o$<%Cz1K{B3RK@{Yb4mj=F znLeIlcY;)u!|kD*{jp}t^vt}A87`FHz7fihqtN{cxG-{}lho(8ZaB=X&&OuwgHoSx zH2Ke;7i%_70pXq|dlS{$*5uN!<2-{xa#^3!4;wvsY2P`_LX0Lc|4%}RBB3B z7x#-OSGklfLgvafJH<}AS{^!2vR9;}MxBzA7Y(>OpHlg^stc}(%Fip(9%na4w4q-( zj1nLryBUonL0#9o6o)HcuZRw1utFlR z-UzE_t9I<9r+gC4*13ywt|FP{c`i>*5#t2o(^wLgz4%($qM?` zh7Y))l@nb?)L<+9hLD!=8rUQN;IrbJ3&$=*_5WgoQx=~QX}hPdBBbtt3N{&P_EuoIjEZ-%U}+RDYGB>wm}ms^O>6yYGT&wSqlU znE$famMAWEw8g1qz(=P7uX;!(yNg~Od@GoBrfm}!lOh3`IYAsbi}$bnD_Yt+hSaP- zG4*d(4avE_F^v_G$P58J^P)w?7cEHCk=N3hvaor=94h^us49}uoJ{2AeZ`*`ZqQ`K z>ioQnof8)8N8`)cMV)APYa)Z}Ln_fX8N+th;?}%1cyRTW1iS_fyv{U#SpRJPtXp?p9XGtxNeqq zgSyY-_fH#rmEdGSDyhZhp-f^<&NPHC@8du@fw~YjcE!doXy19oISK-}>lrCnq#H($ z+w=OFywdJS-Re4d3(b9elxzOFhF5PQtRV9WNUF8=92O^6MLXROLPA%DC@5fQe^_=)kIB5`s+2)n}En-a8B^s6&AIB6?W*CL2sL= zbAmhFY}0&xMNHs6feRpz=_?whsg{6--?&d7^fJtFc39y< zojU^^DQ`6~KTl916TYbCp}9E&EwFdY>vJ>g=`yIbUn*ct=-=17fO3cRpl9V)N~5x$ zOzSIZjiT)eL`_8ZS0RegH%B*KH@$Yp;ol|pI~?l z#~l$>u&BrZL_VJ)XxMVlPOt@n0#mkNr93W-B)_9u#w!9SWM|(E>@{<$#p-H zVXsC!3FIiLw$896J!i9@;VQr|f-K|Ny+thv7~hF)(%)xs3IAw$FjY&o9R@i|=>y5k zXl{|Piwtdj=vc6jqK#8j%L4Xf27(AHpZ6F0=L+yO!i|e^!ZCkxEsU#AVsY+^?K3HM z5u)b9Ey{eR(sDzg{YuXtdB*Yddh;0PDN`}W2op8$G%*6mxYdqR`-i)HzaHzp`zwlE zdX8J*!qc`e30FbIg|Qgg*RA>DC;E|smom3zC{r42cpU3@$ZbA9qwnO6cPMg&><&ynp07_;s$_7*6N zg`8YN`7V)QAYV|JkNpygWb$@Am7Uq&OUi9zEkvhcFG-_GP$22x#q#EdtXDgR*hg|m z*FE`}M;fG`b5e&Iv`aYcP))(S{sJg<8EqyvA*bD|7B9y^*-1sLvy8%hj=78ucl%x; z<3>pfg3sP&`{s%AHJv~B0cQI#O|p3dQK0Gl7ibBkivT&F;>i_jN>K_ll19KicsfLQ z{u0iySlr}iU`)C?DAN&Jrk3Ad-9o%r%2R;~L3Owm?r}3*JX41cAR+p!ZNp`L6Th7f z3P;fBj04@Codq1m9kZF<`Y4LIU^~ZCK-|kzmDK^CfK4UHNtMJ2|F#@6rlGh1qgO>d zM;hRJnjj7xF{4Z}7_lLinzTwLTXWAonCN0V#f0jG$*HNSm;CpHQkPZm5mk$eK8HSg z4O`+AK)GDvU|b3#!k}9jDir(7aGc(Yt}n?=@^N2vkK=C+VcCR?^h;#-Z$w!Y$mxkE$ArfOsGIbBKB1PITryvN zZU8#kjM1%Yg_ESZW!-3Ybka>eZk1-fN4oe!^3E#ot%u(2?eKxB7ruVfj>^>bT46UU z8{mQ4ofZ@aLBLId;LE}D?rMjWA%M~*ir!hZOeFX1A&FTYdyLM^sL1%#fc$2qZZO*0 z{W2l`7itU*v64L|2^*h#WmGTW`z{!g#BqxKY};Wr-xKKYM0@rQ3MJ%qezoz3KFVMZ zx#w6QgYF$N@_|V?4Ob!1m?vER-9?s>V<=1&K|8j;)NeR&_u;#6ouyug-|xU~c?WWK zK8C8R$(rE;0zo@2bIcXwOw-RhL*40|B$mQnk!C`0Ow;bwLbY*C{qjA&u)me1 z04v_#v;>je#x_Y6#~#nlq2zFsaE%Yl z1Rjpxh*o?a3ZM~NsW_&|+J-D1;YcoJyF!Y1x++Mb4|UWJF>e#UBBcbn67~ zO@V5MGWYqm!#kO-D5FGi>YMD&Ac*!P^}7ox|IMzL;O8No33ANVxsF&u3}ZkK8W zi{n?+`*7+)Ys_&}hMi7U&&s>mV;2$ayfoRSEWV>L*)$Kt+F0_D<23zP8})ad9r`+k zGDZ!uru>eqov^im9{R;v{!jtcC!{~`k&=t*oT|Hw%ZcLH3N~^JY(;*J>P^ zH~VzuzT4otNxJbuM!`7BUjx~J_H!bN6Myri*NDp54oHeVb@6_g6r?Icon`u_MkNX7 z&G!9g1|r{%LH~l|{b>Pt`OFQ)Kp=8|ysTe0NjZ^}N=a=923Q#~G@5Q|+3VGF!JuYnXB&ZtDhkNEw& zdunMuS9(j)`oPxcrn$xgACZn*@tPP zf`0A!P4M9uaKqj_d*(M0*FVuoRb%hENSu=4z!Z~PzP%prXK@81Wb24L{e{!1Sc7KG zZ3uNT(G^e`-Y``6ID#c-P}EJmm1|i&y?yU6ehZRk8sXe zP`q}be#0HC^X2h9Uxz4;EWP2Pa29a=+|Nos#na;vkh08`I$|D|h%OLvWlnz1YYg_f zC750ns2bv3TK}=m$H^l2+pwq%_?4=cL}oNT+ErvafeK ze{{>2HfR@u>A!AlzbiKf7x-ephF)=?K6?VXU~mdP*!20kcy7$Q^vUX4=Ll>U`LbL| zyfEZmb4uZf5+>{Va>No#+*9BUbivRo9PS>MB>>?60JU(2y$G!B)}!APDTbz!6mnkd z^6y=Xl%>yuR0on+x5oOl{cX;3Gsa(6kJr1kcEnqh{GTKV;WR!4rV!Ia0;g47HZsoP z!NEshKqL=BcTL~D@9ioUB0I95m10-nMHd;+{dWB(1zl5G)H0(xYTE;Yrkh5JTu5gp zwrp2yECvs8%hUy{v?2}CcF%HhAT-)RIhz0oJu`9yi3a~X-2(l`sZr&O zD=&_Ct~5y1r($Cp%gQhG_nR38k;t1xBC7s*2Nf&go2UL~Y_`3g;|%^+;CUZTLDbb| z;^F1^c5RfX&$(Qb_tTsBA#uV8Ro*XEG-92gIj z*wr#$nFvNNYVjipn%*;aBY3<;QNDH2cBd@wG86e^Jie#x_-=tfO!~e}ljKv2EB^0U z36Dweh|Tf@@*!Og8CA<<_x$fOvYUsVGnq|9XL|-BHjSLr;R-p9LE)Jpn%xH**np1+ z4RHyJ9#|c*naA?tY5XdhuY2|HL*$0vosl(@D82G!*g~XeANr=TkIUp-t@g!z%aINk zcuOK-`r>sHia9ABd_gmLpS$fYJkk`D?!i&kW3*1b-lXu>hTScFd^#c%JbC5SM*vq$ z&bOn7Q&71rIiUZs50jl<2Z@v66Y+gi^`B6_Q9qB+^G^jFj$IIH{jK)>J{KAk}y?|#CPuY4Z}&;sZHM}TF3hT92X!Y3G1xrJx{$}~0f zPN>K@rvLyp14%?dRCObMUPnIi=RC_Y<_|;&@D}gzU9{V0vUQ_^KHU2_eRcCyL^%tI zh+$AXBJP~i@FNzHy8&?h;9GoLF;qfBfkr{gG&Zr9jHG;hyu@vCy#p~ss61; zpkoHw*OB0x5yOVb&ERKT*x0X^^by#FGUHdoH9ol1&NUKN1<0djSYpu)wgU64)7V051R~K+TiI zfO$Z-0IV9k*QS;1zsws*=X2^V5y|T)Q`Cz;I{onB55r6OyOZCB;-+>VV+WG^AFUW_)_yr8#_68TwDKf!X^z_LSXl9y@&W@u;zDMpbEh3L&abz0cN1sQ%qx7Q(KTe;X z|6ak0g9+;*3--%H;^_P@G&nfy@HwTV@FZ_i?qOA z2G$w7hzgA-Vsvgq(6k-Iq}yMvjHpvM}405oF$SUu$Po6piuAN~!WV7*URHbEj%4AncZx^Ia)jLBj+8taeKu_y=PlI5~06G-!IrvXUx#mB z;CqO4mJ<<8yYgGXoqN^`c$$8H`5B+NivZL*0;x3n&RFEcm7XUdRT*PD(_8O9)SQ-hPv(YC^GxDfWtQz4mlS{V9DH6bGj)L zybukZ>RAh?&%#dT&2{ChQmiD5plR_+xu znzmyCHEo|kt65JetPIk0`nVcNW7^^))gKTH%2z@7E94x$S9_jp++{`4zU%uxA|m+x zNAIQGeLi7hpFuzN&=S%$Zb+ChmA-#WBvlowAXa4s$re;goy#NN#4>q|)v{M*4xiH@ zkK&YX^8dEeBM_iWijjyLN(8ELc9d2suv)QB%%1S#x6#JI_NT0nLK?*L-LOK4Vd3R_ za?_b2*6YQWx61Xy?g8%5dsYTxQ3X7O6IRBs<2E$H%4`b4kpLOM1Ep|%!e?Qh?Egjj z>65=nuVKMbnp7pn@Ep1UW>wTuJag(c_W&-WAD?vZrB)p;`re}tZYCk;;r-6v+cxW0 zW}9CV@FNsIP0>F=(TXZXVU0eCwAScP&SX7~=LO5mKHXvQ3MRRRgoXQ^pWjG|EMuKz zt_h?9z*`&4BQ$0xjFijE!jSyN!J-lB<0m!~tFcOoYxvj)ML~=kLnP6h@cG+&d`5oNA)v)o zLt9MzFjp{98H_Fy1A!vA4{ z=bRWGS+T_L{d`u+aDkO`aW*pzD-S8cf?`SjdR5A{c^>8mumH^2zgi78%$m9;OI5lx z9Nd_u72)Z)Twr@^J3a$DBR)QnbI=*(zXP)D?@Escwo$cD%5`{Q z(U@t9Cy~JqLp$s^{uEcWUOWRqSp{}4XmOWBEj)9{!aw($YZT!=biko{S$DBH_qbUE0O<+J&!ma#D%~C+~r@XX6#X+7sCyxMB`zH(v zWR83>2%HFkl$p(!2q77g)BzBeQI(199Ucl%iig zlkMj?ecFJF_Wj2n#DY`*@Z$eSFVA1cGBSRjxWjseex>~W;ro0r`Z&Ehd!4RX04i(z zAQpcrc-ObR^z`5v76A)L0^|XoyXj1VT?BzlS-;S40XNt#ufceS@>c-N_Shr2bcja! zw|qom2`4iy9tbbdML4e>i0PcE7=_3JV9h6>t;};=7L;#!v-{&HJ%UkrQNJgZn-R;M z5I#@Vj4VNP@mYkNB=O&`G;0TLaQvQ3tscZoO=z^I_J-dQXdWfbu3EU(*d zzx`}OQ}b6}{VKlM|Jn1uPnW;r3*i}?F@>3AUoyDm0xf6k6eR@`E#=jr2TAH@Px$EPR3 z?)@yz>0h+g85!-m@zWOL4`TSkM=+x=lBh_HV7ZrTWZ8GfB7iSn2UG-TVg_) z6lW>{ZVOGXo{1Nan6cI(n`9X>O1)sP|N z9BcN{X1T>bz4}G^ukZdZ>2II?EdAdv{x81E_rnb}Yq$`9@r5q^iFE;iWM;{E5ORxQ z!GQqi0Ko+^(CvfB;OFPYCjRGV|0Ncu_$5deNWpVOR(2>U_u&$(a`M?%L@^P-^6b%* zwEw{;1a~qwz=D{zT}1MFn{|N(6+D0Tfw3rMJs1aKFdx8Li*BFiczGyJ1o|9gRQxW0 z98V)dzFHtczFne2S)%NBVFgex1Cp}|^sj-PLCke3@0W#h^~f(O&?jI3quZU=VYe!? z6TahzbF=O(T0IRn@SzG7%C zJULz{N8}YS>L{H$yP8$k($Aj!9pa`6ntENrafYN)>XbPveV#^tPQ#h9tSDE#T z3_?I9aB(;h5!|oM3uQSxL{E@QUnzWWu`Bc zOYLk|)o`jxHx94hi-`Z5M6+JM&ZHKBH$e$ z=hqJ^^e7uu-YgKo2dh8c@3V|J&;BxkM*hHOWCG9Fyr-M*r1!et3q;Wi>0**D5~(67 zB5Cq%w%GiF!8h>KPrjO1B6YZs%H8vw=jp5KFHt^Ea~-}~amT0NB9DiA4(n`^hl;#@ zlBN=`#-?r&Fk`QnKA(jT*aWKH>P7I2n@qGB7njAmV`% z5SN_w+m3Aa`#uWVzGz8Y#C1+HJs+9VMJiN4ArT>;3+ks4;gRE95i4_s%x=nB%F0;S zM#qGXkP+@(ms;DTPPN$11HK^y3F-j8&whOa7KnCT{yKXDY@+Nve9ZbeY{cYJ@D~@v z{Kp)*UNtQY+JvGfpr!Um!!!(;&YZc*olCe0XjlEdxVb=#4APGu|7GO)BTLO-Lj{7! z&YgNS4lhhSfkipz;8pZq^}dP^PTVpsI1mutZSAFZK7NO968|~<@{3=F3tLg<;97?Z zRb-E4R@AgsYU~iA)!_tE#Qo#!az$}h{A@w}PDyWeH(2eo4RJ6N3HQ2X4do#_qYB|R zOV7;sRJ_gRltU>01`HTNH&td&CAM!ogkL<%7?O!LXU;<1|0QJ3Mfc4*XufnYSr!W2~5K~UHP}USb za4elY@%0mpHPQ!a;h#&mQ`s1CK$~LOS^&G#-2v&l!4-YIZr?r|NeG(?2x;5;IJ_n< zU(fb(bJ+hKTh0wza2KqkNyG08IC2*_7Z;JpC^7aDtsU{nDzI}QpOr_U17vtd_ITo8 z^XeFksD(!katN*Rg+)$J%0IJLap_K=fbj+Zf5xhpub6~kTbLpH0; ze?G~GAQ8u1ajy)Y4>UR#h-q`-okJq`dENFki)!=Teyq`JV;Z6>XrSj z7}Dia@x#3X5FmdaC`=6axX0v;?Q8i6MA2p$W_}2g#XNL&lr~ma6!MM@rLG?GgIK<5 z^T7#g1q|2NOOZTKBegN|D&&lDhJ^!&2oZ>;zj1h9rj=(?f&-Y-L@V_uGEh-ay&8x} z=b1Fu0Tc<`ug0{n5_nvv;Z*?BYoXvYh>6>TLLZPHKvWo<4Ommn4HH;E+C6ofu&EBq zpShJi&$}-zQKr_2(sNfAah%Y=3i>UVQm7@~HG_%IPW4d#Dnp2T-%G zMI?`|?1LR_eU_M)#T)u|h6Brb4SX@k)5)*dkn$3Why>SY?6+A(l*y${wuNKsogPHh z+r<87oXTjCcVYYwLImw+N=C2_OKr7Hbg)9JmI%K;8mIVyXn{ST25C z3F|oQb-rff$iM%&sBM|d{=EvEAT3g%CYwwe=h&MDfT7N^t0n8#(4YaV-^ibB&TUv! zAKDtgsU}PX57%IIl&g?#*%?rtVs~5D44uFQBu4nY`!{|X4bbc)0g^5xspOGeig@}6 z+}+y+8H}IP%l7E-A^Ec6#Zo_+4eNR0P%MP3X1H%ykm@mO_OJ8p{uTTF3P=YrECB!^ z+t&@IZK@JmP-S!MDxZ?`(#)9|v-k_3 zb5#JpJeP<80{q?29k2}i4EW`}>>ED7U8lCS4+1nJDi2%WQ&Clc2wr* z>>W)QYrl3-`e64W4Wr3EHwxi#J!7M96#!U!x#Z9^~G*t zZ0c+PhGyM^od<+mqgk`A@Jt(;$fJ9XIroXAjdm`1zkdcp9$ZOZF62Oq`!wgo(I?dFSR8 z_h|Pn1xM#_`VctEKFV9H4Blq$qsEqdbd}&%Lfuj9t}yj(w;So|Nh6&-;d_Ed_|f2^ z{e2K-w*|+;CCr%hi5Yl#(o46muTZXgslj#a^d{9%pap&fym5k z*m-ez0ph`ZSd{62pprxO?enW6MSu#U$fKVh#*V9;S=WppMbS+WNdBWeA&Px z1ZVEBIou+Bj655I^<%gK@l5lZ9%Erm9(5v4FJ<76}cXB z>=#55`OSgLu|C*4jK!H%#Hu)lrYy8nh0+xtd$8^u0!gl*+&};T=34*H4xXkjUVRxm zh_wm)*aCQm`v+|N^gKP@eH^yGTfm&KA>`R9+tosiuHoZG({5k0#jYZ{x?K=sbF0m6 z=DX|&-eFn%Im^kR)B>ansPP`HzRwP!w*bEPu$3;KHPiW{8f?FjZrCJpvJWz|U(q%j zNje6n=t)Nad~ycRSw63E+Dr8lfPZomz^_to6^^DQpxlp-Nf<{*FjK$h<*`crrh))} z`||*Dz`6i34}$Vn5W&kp0_qFxQVR1${Hk=a!hlp|(=vpT~OHzm(HpuAos>l!vv9`J^@c=m_P#Rbh~zU+E8(FYdP6lhY<5kqKEXMJ5?O0 zLNS+M8L~m+4RZ@O$YO>9F2s`U?BBP|hEO8Lgf_a6^kcp}vd%F|rV64flxrdM*g}4f z{O>+|hs`1%#RSeh)6}nzj$&!|TXqjub9ZBlb@mnN;?Oa8v%y4>ZE*$EJ^cDj+So%u zJGx~uiu0Xq!xkPMv{Ua8W$z)sN67xCjdZ;a;1Ag-`hXQwAF|&dh=7L4!r5s5XRILz z;Oke2yMpD{kC^I~?JN7)Y@>Py@M$9&GYC+yfBK$f{nyVyU4b0M*!{P^{@>xmDn*c@ zbRCoqA{6(T3-}zi%AmcmeY}bwot`7BXUH|Tke}?ZfwVt1j-H_ljMeVZaQAjPX%Der zeM1D`kex9ar>vO8b}Vh=_U0LrNH^oOdE(abc-I)XCi4RuauT?XKbKFPA?J)HZh$OK z>hy>>x{;+jXTYBrZs(mL)U(%Rf8(9*c7$HR27>Zc(41>`-vO6#a~U=luI_Gj0iu5W z3J$1qxC(pz@lI`X7jaUwtP#Kxk;I55L%Eb=?bz|u)i9xCETe1YHpU?O3`O>yg{?;b ze?)M{?Zb8&Y{3oLKR_-pz}y>cF^P1@PAgmLLo<+<^^#HO1}Jt7cTR`GYal=sSFuW8 z6>e9>b68<|*)qWRX?IeWUh5gQ>+iFiGQf^R^81CT>Sv4t4u zZ_wd$^aFMn9c_RJmv92o@9~6nu2e7?L{`bF!$RxG{H`uqcP!<7W2j3DU~k`@IM>;D zf~Dt~XK)=L?fhyZci3G>uEWcSa|QtKLgDCqC~%$h-o|@b*~9E~YdLo-k>WMutWQ9% zp#vBWX6#~^bqufyshn>UEk3^H1wn2A$_1N0-aEjEzA;EK0{Hi6?gLpq`~M7a?%d)U zAHoTT0KA5xw~2|zbU=7TPuXs_hpewNUMF!clTW71g(a zA7Cy4oZ%)c{R?0Q1WnI*3MDWDJ^wEkD1a=mL4tf_gb0uDYPv|kU}RkYF#wy@Hg!Yj z@K9;1!nzd`Dqc748-$|gK6#r^gNsMrGXS?JidqU|b`|p{@Fxu93`*Bhu(`7Ei6RHU zWDbBH!MmF15F5zsCix?_o({NkhaGozPOHG}Yh0lEin)OZ?+W1NJ&N3u*xki-16Pgk zL5Fc|LIXCPII<4c>vazx>)hJrW6#*+2`UCy#{lh$S-Y`T4J)Az-5wrxfBO(^pNX*R zXYF+Jq@Au|@3*l0@dE(=nEta%*aV_FJ-<(9N4HD`quB%Kda!#CP(=?vNR3@ZYV531 z1?g*W1r!a&xX}26?F)kv=;gV98ts+Zi#yt1KF?`LPZ2}lmYK)^MW~@?ji5}I8zA4I zgDm^A=P8^i6v-Y!o0)@rBlqe9#po*{CuZ@DG74Ap0Oh2o@rTE zAJ?0wnC$McJ35M}oc{`{?9nH8_nM-=xP|LI8#~{l@b;nhKIYpH1n;Bl_1PzRz;Aqj zNx02=i70}?;gorj8b0wDdHX})a1ePanOrgqtB0J>_kvUW58mxcHz|Ei<6`Cqa;#4`- zMmOjoCwpyJq7lM)-JaLe-R>;isvqD)T4x~dF()^Uk*h?^bl8o1O>Tga$eOYMp5RE8 zRsg9L;>_Rn4%~nsi)*y#Awee-02{FbwA#Mr&(r`gT%_G@!|D+SAgEqRkz7yWI`7;@ ziF8%Z8|vsmm3-kCVo3#Ww9e|DxN(O#ar$`cr1HB%>e04G#*YzijeXZUVoV^lyE_06 z2fII4iG%9dV`z@W(!2ohq38j4$0sNruBrI6Ke!TIYDX@cWjW(5Q22$i7ho!3<(YJ! z$2lWKzPCt`Ph{abM1UDReD>|4AhM80PLVlA20CRzUeGwGFw$TEVyc@Vxl7_4eZXG@ zkuVjjk(*VtWwqI9mx0iWH{2v-3!wK+Ko4pkg2Yt- z--YU%hH}xi^CPB!i3S$Hm@e!jF1x=4iDma7090Q=&KGC6LQAiRAFlqXhcf9Kv)$!el1^G+D zf%YPRx{m7+*}wiZF$FWWwk2M1i>Yd`gipCfAS7*4#)29pFDeEX+WF-~^a(vmIvY&=uUZht$&JU zeoq=%@Es7_hp&Br@;Lxuswj_!ZM6^+Qf32p^3J>1`Z%8M5G}Wery+X46d%SI${+b2 z~XS&J8r z3>F<5oG?IQRKNas#FXHO^@HcMG=5=})_26T@ZjIHyvwK62Y#LWBTKXPyLT_~D95}~ z#Uj$d1!DwO6rY$eJiletj&5hgL5xpl981HK95BA{MH7DklqGy-4D7q}i|2+P+=Br4 zYh3#s1JG-X>jsc7Sh%^u#oyTlVHDdpTaK=Ary1RH$mQGt9}^?$gf?-~~nS)AFtC23lYU`;S0}ZCRZ@_$KdQWy9SDQuk06@~sQL z>H-Mh>=E(~Gc9(W8VX>jBw&r-YXtEO6x41-BPh4P4U5m%f{TBKmaenBWexAJl_>*a!E!%Mo-5Bo!x=Xj06};->T^-GO z2m@gk#?tcB;%%|jYQF@(=?!)n0n@&3m%9Nst;54p&?&F#R2J(9XJk2ykuSdgpCvzV6Nzgug*PHvtC^=4bxqp{Qk6h^^TRicX$mK ze*keGHw{+w-?V;Oy>B*sWUzV1S-d+Ipsv`JyL`!_RCegttH)zppYSvQf52S+2;KLr zeZ0fNTr$ABV;^CUG0cYP+ynFaz4tYz&*GP30H<@zf?UYi-~ltL$KVAtm^EOjp9M$- zxIEMkw^47SRR`MWC_OXy(aP5_4220dZ|Nx>x&T|~SB!v0KN|%g2`YNNZ3KFfG9Dd= zMVpO4g5IPOhLO3tl|u|H-#Xt-U+8O7ju8O=!Ko2$_3@aWd}GINbHhp<)5rH5E5Ep9 z%Eu0M|PAr|j zV3f}6;))|!f8zFU0Qf!pSFG88dPNs=Op$5zhk);5mG9)e1@sFJv)(g!WEW|*=awu6 zu*LwK)@|6J+G8PS4e&0Fr)#sX*U)wOez*dJ6%=5+j}~exHoj}fRvBdLdT>Znc^jG*}yuQ0|1@=+NGWQEIz zreF3?!Bn@vfRT~78&0;OtRymPcyzasY16~8#&N**Zukxhd0=8dI69Z0D5so zi^EM{90d(26S}c^=agef5UY4W5T#gCUSDpanr1Rf$7cF6R z3bKQmVFWP199~+HQdvOC08~9@hFlhp06pD8^_8yav)q^2!iJIkBObx22yv76b`9s1#V^GgIz4U>#gq9m%F#@Rq;aL|5QevIT35WXn{vwqa~w20p7eVH_Mp*Ij>UCtjmeNd8C>6Cl$)CaxkH89| z3rQ~R>P~Zf%uOa%%pTb-+hgjuIbqu9VvBAW@a!%=68{g=t>YMiX94-66CBPw!-d~s zB%fF$;;;IxuGsCn=g{Xq;Qxc&yi+`hm!9JtE&h=f?tDJ(-oYJHx{U^xZh$)rIS%k@ z`9s%_j56j^u0Ib4ErwtrFto`KO0e>G zPap7IQQYWe^$1QGZGjkpCz9Mu@z}LT(xh!UmgX_FGsX$$^!sl(#p6Knh)tc3$osc@ z_t+7<2GBon_1Cs|c%P-E<58T;d;ZR89>DAB)$2)|jr$rJBIS9?$iC{x3eRQP6Y&X~ z2(7|ji_gF+H^3@g1?G}AK%DfcyYrlkDfuZY9#x(pXC9LW7@!v8!Uz=P69X~q>;R0$ z6t0vuf2>s)HIf3p^l2OdZ%p3OI0|H%S3O99pP!x&bzuy#AGwgH;`IjZk$d)F?LC_* zF2?u)ECyx|tQDkdzzFW~0RIETb7&gE2|IeX^xF>bt^sv0r=%e!t)8#rVI5&oQgTBbPd5JSq$Of@_DsfP9Qzf72E^t zFK_o(zw!qe;z5`p{2;)L4|2>pQ#x9_Yxcs%%X96eX@~c}KDY=Gm zRD|!M4{$4vmS)eBE(~bWkKWPH%wvR;u1@NCrdfujC0}Bb0d*Sb!{8Q!zyd!BRPYR% zEmv?o?qinlr7y3$jFw-3%7Bz?BN0$syR(FF#r2zB#!wHgr;w(6-KV&kLyLr81%CeQ z2Ept;l9z@^%dF*9^5VSV20))M0Kwa^w48$|0P7mUmNkP%`h8zj%ee)*knGC!aV*c? z`El_n+twIYbh}@{3SgZnsNzuQ)JnxHZ^4r14bj@M!i`jT+?TXn0A(F=WmL0F(^EI< zopn~u#5IWIrJALlWw_GP1qfKM8Xh1mSI}e!(UIxzu zlAK9wiSewF>E|}wd_yietqU)o-*kh~HM~RRKU#j>dow6*MI(sN>Rj7F#@!DmX=LaUarYAAW7I$KCU8~ zm7c+sMD}_!PVj}v4TkV^!ylKr;>})P9m7D}mUaNQ87*8lj{(??q@Nf37*$oK<;%K7 zmvdBf7qZLM097F^Xg)3SS-jI4uvE15m}(eubyr;sUS1eRT}iEmAjbfIqBI?+kP! zXF>cp@J&Wm@VG{(?n^){DeFTKB3dhz@+DGq!x4~g=!gVGG>x$N39KhJboZ8x{=bwWr?(Jv^Kwd;@>zk& zDfE<0weqMlLBf_f*Y16kd!)^AH8rog60W>PRck`6rGPMrnh=&|Kr9)aFQ+#rb?uzD2Hu6zv?ls68Pc>vrGwh8np~(>;UMWeI zBmes=#2H+pa+kzRPpuTa^O0wOwaY(pQ^vfOd~ynG)4lhn)XLlG(3kpmjHkgzcriJ?P_3dj1bS?}5?G6COHt zjAKeukNi=iTFzNwJOl!~!>K=9H)k+F*K`=Am zpxKSE@uIa`M{Rini;7VsoiGnq;ja9|)Ke8w;MwI=9?@x%d9@D1x!Mbiyrr2m8%eRv z^oq|F<1-|6W*i~woHEUoGZMzVD!F1+`FR8ga~X;!Iui72$U&WGYBByc;yWu zy)c9}9w_yoWw}#6mD!YUo*&wTKZM!9+Bll249W*r$w)KoHRu)lzV9dsYw4WhseH9U zngA#!sV(Du$e8~!E{l@TT5dPplrlCOYWS2`=qP|o|6hfZOIm(ml|0?B;>s8xf@F|a zzXc@)V$>WXFvD{FR;UIt0hNLXxGlI0qB3iMq~`&IOP(sIIYd{t>;hBW!D4*YNl--bW|}NlZD@I=%Cf`aRbu>yp=cnaUo$o!ZYe=SOIw$x|TCG|MD6_wW%4 zspyqrxYEPd`IU}J`+8WFi>`pYt6t5qYANp(8s(a&fkaIBJ0nO%2kZgV=Uqi*P#!>q z=*wDyk6@vPArSQ9sl(e~7K|aA7?x_$8iP^w_rtZP2@~|c$Mw(J(4E;dd7P_LTee@-`iOEhytE?`bKeNj7xMK5DYGjLBg zenKo;MJ{4YHFZWWWPCkT%8JLtGs3-?6eHV{&6oXDSdp##jW)pRq zBds+FJ!=zss4KovLXAo^a)}$9fftk`1}IKOiarZROkbi+Pm`Y|vw9Yc$4uCm9Ig%m z5G@BYM1i(E((1uR)jGl8%1_*nB%oaqYdo*i9t9##&G=0@elZ9*T@K+9fN2bb%i37xJbslLePpXn6Nm(LXyE#yYoJ((W)cFI?U%scdu_7eRCv# zM-EX&e6vZ;^ik9KlqaQ|H>r~{p{p*!U#ak#E32zMxu-I@ogK7-Dvyp?+khUBL%!!b zw%BbTc|e`XqB6;#D#A+-R=!KjI=tMaJF`t#oUbOyQV?B;T-Q$G^r|n+P{sC}G{=E( zj zx8hJ0Sx3X|M&0p+dFqOA=({=4!b9AVN6x%E*JB)RQQG)frRJkkr6O%wO8Q6SK_rn-&vB^t7Ya@uzz*ngVwS$)W1nB&Yy=Wcn`XpG)bY_f2w?mZhw!Tq@3001-YNklGvHeuX#kgYYZBm!iM)?6c1j|IXu&KmN=!@Sp$flTSYR>tBO! z`SjCIKMffG48nqh2$n@7|1*XB9D)4w$jBHlg-cLA7Ts`q9Gv2Y*w4hrevZPXsabKD zM?q!~)4(|FZ(nc+sLo)2B~gv}h3oPk8aJ`t+46XUv$fawPx<+zGEIBqSv!CMF_qk{gAafgKxcrbHk$D?W2< zm}8MC;>5&61Pqa#fqh%tynPwa3h^0BqSsyC1K!)jAR`fj5EIDB0D0xg6)Tpmp10ubHy13Jw|Xgwos6&ncM_934t9Kr(9|r7nJH%~ z%%n0H$kD_JDJf$qih4%KXvVz*EYO4kB)XSz^1v! zQQU;KF=@w21fyn+#EgX*DkBD&Bpx#+B{}(}$t3cd?=D+2^_}&zw{PD*d;L38*StnV zd&=~QFO5$gn>-f$jWINL3^o?*k*HZXGnImvG%#f# zows1aYip;zGkg1v6P5caPwd!EVlPG5lgB3~CyyOFmg0_sO$2*1YE~>WCJhv3TFej` zCx*zA7tzRT0P?I8Gk48RoqK-fiCNp%zq5ADYi}Zv^n;~qR znl&ObAq_0dP#84wSb#i!{Gy2yr@uI3#nM+7Y3kj5y;fO+!7iBqP`c>aUc z3*LTr+1mHl17RHab870J&!68lH#POo2P#kOn7#h}waX~%i4!N2(I&$Uv7>^GOC*{a zK{J>MIpfj*%rN~%LY@ba-kYOy%EsLK3rP?KpGr+>Oea zKq@FB5%z*tmwxd4i&K6d51S4)DeMubS(KnDX4q}uazPqAITm@w^B=rAkC}fksArBo z`0A^#zCQHrg9k^?%&LUp-$i1Bj2r=*2=>FNS@D@c3gXg0VTS29e*EOgOk^l#B;S2U zww*ck;Onpd_P6i9zj^$JufKk9>ddwy`*zKpgk|KrFe6`l5%qrrHqrb*7 zWmpeohSI>oJQ?KQi)8*GF~j^z-E|lsU;6fk@4x?k=gyt?E`Id=_dk4l>D0kp8{(Shzxr28uefz`yk3QMC_0#tD+qbuF z{p6$lKYV-X&cSn=4u77S`uX`IH|UJ~0A?iW|Hfm%2209Os3A1!pwXB@F#n2TW=Mn1 zzvn-Ab-~+2{@s9K{QU5ya|iFdb?Es15072D+}1WcJUn!(?egVo$1dJHe(0?`>vnJY z;`2%1C%bXxC>kTljKsQal6u1E)GV^IBges5W_3PJ%hlYl)x3yiqcJ<=Ty@%dfwQl#O&lXNv_!-d9Q92`M&>2ZlQ^Jm-CXrSHb*q?bLUUf*gY&b1>vDA@cR>J%VoVN86W|j}h2!?0ye?^%o#p zk3N_hlaV0Ul(0vmW<45n^!T@iK>p(MNs~T%Z}%Ik-a5JW=7o#Lu6@~d<;s=j=H}+c z9zl0wWAl|OSK7Y3cI@JXn|n_}?9V4nTKL7La}b-#NPHqSW=tIFUt-h%nodHpfBp^G zZICILoSaM!Cgk|H{>-);5czX}yqQG)l0XL3g6>A}=anl>U$z76V}5Fiopuh>KX;z zqPn_@ii*L(!NG>=rl$5!0rnCiBOx{+ZDQ8?BARR`C&yBwIUk}1(3qg9`37W8NB~P# zLK(Q2&+UW1;P>VLF)um((I;C!ZU1>_q^ZqGnOlPdx<}1wbII-#!!Dae#g!DIp;RF0;`zpBhXEF@OFA zL@I#cAmey1wY^V{Gk?C#-w^YS#3Zz51a}5LSUT^` z4a>mHXvYmG=1Bzd5(0TSM4k^@DaR$2^92IFi7#k1iN&IbfG2Zt;HPV9YW4u^ou7OJ zrA=of&yhQEHQ7aQL zGy0qwOpr46>;X|OE-scxIC36OE|ze)Y%W)#X5j087ieVo2Itnw~GkSBwGHRKf3iIy`EdL;KF=!Hw zh{v-4;=;_#Lbf*|OB2G~g2Fxb>!vv5-CXZw!OE1T8y z&NfbsL(}4TVgkjyV(IDy_{wBbDjH14{DaCMku7|lOaeGTV@pjkc4kRQNlB(u(V>PP zD`aye;)s9`#e-unIY~|bcI|@;GHUk^1Un_=^;l|Fbf8~{(jb&UZF7MYH4^jQ;{cfu zGeVY#_&k}ETbKz!OET4RxjM5XFE1~zB-6{Y5d45!2_@v>)Wpfi&r>ITewf^A02%?o zhSUt`rvNn8bKuNK8T2yy^Dj2-CI*wOTe19;6oUZDrCdl1Io%y{b!J`$Y$bLt&kKG4 zh@HviN@P5!ZnFE^x|1GkKF9VNfJTN~C@=?pkvnv%+Aq;yS;AV5BGC?a(E~c@GdDJSfAR~AG z-gzHrWHmvZl!*F0Vu*Q`6(bJ7yn4ZgWoxND+tgkA&K-m+6EHLSxCsmMFb`< zfea#>Cv;mPYGFox7HawVdG@fvEkvJ$@n%ac3cdg&B^)9NY;r*cz8l(Wn7WLjjt+GU z^h=AT&se?S-8ECuo)NjshSU5{j%{s6V|Gm`hr>UAn9{g+uln!+@HcfrybEAdbh)U4P* z&saKd!DgFI&obBp9$^L)3xTbc$-NSZOU@IRL=Dv~%bPwX7G!VTIe2a#v5UE5{nTY| zuU-K{{dy9EnuSf2#w8}Cyfl5~(gp9XeP>4{+Gei2apu&wOKx7g3f8PmeSLiDg&m%T6yaxD`Q-pAb2smdm7Eu7o4v3;3NaEiFw= z?Ew4c-jkOOZaZ=SCeo1|@2mv^0jdTwr)X$2Ax%W;SpD|0_h+9#%BY-m=F+#vFI{1z{$U3knSO4r9=1U@}8&LrCE^z%PQ>c_o=rxrNIG*kY4_ zXXiXWU%qu~XlRJvB@(+zCB<8|aJgQR86X4k`ZZx=(4o=lP|Jqw zxw*NJI;gPwvkKB-Y9eZrlFKgKvPHrXck%@M&c41jxYw{BP#-yPe%GYAmD}H6vjOQN zB_RweTR3Y&aK?>&YBJLR;-+kpy^Pvus|ohPD*U=LQlXG3^_q_go5D?g~I4@8UVO8&8pLC zI*b}E>Q6wh^GetjxfD*OwiM5=5R3SNq5fgGi?)Bs<~QaX{w#Iw%pL33z6+tpqM0=l z8ZHZy5>k>uhqvI}sk0$;<+fAbeslL8K)&AGTvrE>DP|?RB!g6QmfxUpdlX?$M>rS? zIYJ&IPvJ59oT0EH5O$dTTCG;=cLu^5E$CN3umQDOCgTF?EnAA`msT`1_V9*=`ui_m zz4*(|fPP}yxWt4g=$R+ZT>9a{ zJ&=Ljjm>oxm^PFaFf!-o=llHzjm91dg@OT(!x0Jw!-_!AtkPNaWkp3rWo0>ft4-x| zdW?!d*y8~mA4V-y%auqVh`PD4r+euB{o5ChU%G>!XHO-e$51OT1Wj%zLFg$M^o>*B z9=~|If4HYxAm{>-o=-8W;h2&Eh#esiUSrtd@B|>SRbN(CR9;?QUteBc4$$==;vS)>a zzuwr`)7{D!xezqahT3f}(P)H1yF26%nl)yp(daQM6h?>7rYC@@2FT0wHlM@c3R^qU0qzHKCQb8~YS zPtYaiNVr_C)FSh$-FCa(9SXUH8iT>0aT;3{im=0BR_TiXa4pslcu|?v=QR7AidIF~ zVKDgN@R*-b!nSm%5j0247x20oo3FHe83R3WeDc^aSSqq8btEN>NgmIHzO&>4fNq8x ziCqFP3+5;-a-|ovklU_-$d0h0RS`0q&1RdPB;H6i61=?3W;SbmPK75NP=rI^hXsR- z&@BVW$OcQuRz8T%m8LI`UDz8BnodO)xorS<8v*oc3i{k70vhfkR*3nnJh{xoms^xl zwOuIGfaVMa6k(6T=~Ma4w%+o3#JiYkBzS$fUgcD&d_kkr4E~NWXcuZUc|yBdZc%f& zE|Y-I6V(mYH9+WZaOmf!Oq>k&Bq@EcFm*&jAKbk6!qv!E#-&=yjGqVe@omHpP8ib(^g`&f&mRjUIo`}N{^Ti0d z9YViHK)*>tQ~F?08)0L{j7^?Aamw>F^t!k9UO0C7R$s$lDHsbmCK*TS?chbcZaX9n zX|y`4)#@`kY*jX2)#>_;1n~=4Be^%$miMa6HmfQe)LC>6cMGAD{=8% zrKP2V)e!ovH{P2xcOMNso}mvGF|x#g-b6rmHo)<_m;;tpY___CXAJNHpzqRIb93}o zhf!5krBd|{)IwxH`ZCo>?!}8Yo-Nm#RaUDxV7BJuhSibO0V^!(Bq0DbqH%@BGUoKQbM<@W@1LJ|&* z0zEc)^6y{7px=0F?}r$)T*d|CV4g4}2x9ZRFG7U^Mhz+P~ z2lPHwRh2K`1fk9W6YYY0O^D}}Dy3Ym(!`lxJin%TIR<@{f*wDX&<6=ksbf41y>H>9 zIS{(Nzq6r&PzPLGWn>7A9t~nHD$}b1W+;w&z)TST2i|Dxjb~5WK&Tx?Rc=lhinU+Z z(cyNhy;1;OjG(XH-3y=>BIs9{&_L8IO44I-=o=*T*7mmk1~5}9Rz}Fxl_nHG&_zXM z)}Y5$Ri)Ao)WR?$;(x&!i4CdidqJpGMu#=0tSl!tH#^<$R;YzSyHqMw$~ncw#W?gP z0{X?@PXy4ks9AJ}heK~;LIZWk-4s9A_*82j!DkzS{h?zkCH=+^TDA=zo zt}U-J+p4OZ0Ur_S0!_F>n3q@L_5x^badAyeTxh1KS-7Mr=%d^AO=3WADV9W_I>59) z%isz75wy({v{l(uy=OPR@&bkYZ;VD`zwpZ9v!_+QD!nZn)FEiCUE$VbWaMS4rCt+^ zbj|YidnEM1Q(sZgxTslJFFig6ddY`d+nZpdxf}(tCM-xV@H-V6T@FyR83wv)fMiC< z|A{q(O;T?h(EDuqD%kCn<>Y1?!X29Y{QQiPOg2|$kr2=qmW%>Djm2VNy)+KJY0{)Q z>j>x;PI0kBBxdde5^2;MZkEK=ucpi?Q7z_PrQ)efDHd&B53evN))3qU^Hn$7} zT2*CJ=?6B_%p~!j{`99mk=zvZ3n&-$dR4$_)#s%7Jz!uD(2`U zFH@~FaU=lx*heJvr6}kzWcNXJ9^>Oe|7CfL$t97*^fe7lW+H+f_i$(q zhwr5%otBoHYtjoE{>Oh%%mDTOBO1acs9~sISbVk@bbGq6$LtCFb=m2D zyITk}?X}2doSN$X+oMCnK5yE?p~W0AACq)$Zf>s5VRQzZHZW1z_{s}ppb2Jx`@7%$ z?so(>L5+nPj4)ks|030*!?l=mSC5 z4pf=V=719{26CaO!2(yUmda$J@7m%*GlxWKY#9-{y2T_m@hwbfox>OmI6%Or75~{pBIh6CMS6k1HZFMP%yC46^gc z$;nk29cH7mYJh}i`rKtX3|LTb0#%Vu+3k?!v0e;?>-lPDsWNi*{( z=v=Ea;L|ybHURzd%cDs;TGVJ>ecBcbPz&4wjXR|FO4*rg4*$F1zZ0~}B#X!`UK%>r zs`UhQRVt(T^#2rE6>xZhNYmNr2D?|PRA)ly&c9cr<#K+9MaeFq2Sl^Nr|;D}!uqrS z6M1Z`?{yk|W}^4VPS3Y{yh=BMZu$E_*VM@55t-NGF3BLE)3m{`uB@oc7Vr_>^iN4d z*n1eAHdQbLw{5`c)@$*yGc&njf#A`gAKK;B)QC(9nOCV663}U2W?EHLUZi(;s43o0 z>GIC@I>R=-)nN4La&yr@ZUK^(h+4Zxfc}Xi;+~osON3{UDnTZ~5iwiqFluv(%FByv z0Tn&Rd*s1o<3N?stS{5+f+1~MT3T9qmPTz+qde@a{(C`-c?z#ossxz`k4*eVhpw!= zyu4iR2=>;JqW;g(hw**rt8Fs0ftyV_5 zyBkJ>e)L%$kOxo9Wplkq)amJlut%#egC{}ypyPBcrjL=Qrt~VH_O#hp1;mwOH5)Zr z0G%&Xd(~dKtn0fN==k&Lhb;p3RJ-I64!e*om1bt-7o?|ag$lDS2Oh3~P*?r5MZhbI z>-Ax0FFZcV)p~;dg7oyP{5-c(YLUA{G0-C{0v@*9+EeX{h$H~oYX`$imci5Ex8@Ls zZXnb@ZMg*kZ7V|1I;T+&}Jgnf@U;l2mlfL!v?){mFB@MRKsQVWKc{0w0r=+A~* zrcfWfJ|hhE%ZyF2I6F*;c_SU@qOzP^ogp05%o>Oxm!vb$+-u) zEw(InFAwfOM(^c8XtdRf_VP>wwA5=a$rE;j$sKhf)J|g+I=Wz<5WVoi{~`YO!V9ku z=S3T92lQd5zK95PPHwhV6ZQ!6GV(Ijaw&Jq{JQU2mVY`rG;`N<>& zxc%v1P7R^U%gcLJMrYOOfwN>A7&uL}-rnBRrw0b8PY#^cn**xeBFKz()eY`|y(F(B z(<_&30nmL-81&Jp%kceas`Fs(ixJR_P4%IM(qaT{VQ1Pc0eBrOHwVVLsHnF}6%3ed zRaI3sn@y$i`BW;E4YVrQ(8qi}mCaVA*XwOQqr;{z1IYCGg~wi!$+lRy5PG=j)2$ya z8Q~-#fpHR$6a$?~p1ItD`_t(9ftu}Bcr{sQC6JSolT&5YnS%j=(cv`vRJJO8?`afP z@^rkm7RA1|%BJ#}%}$3=;Z)hIReJKIC_PIPinuc~xj@oe<_~svH?@<{QD-h=81d=YEU(xto8wSlhM~l|wy;A#P`eR7{e1b4FaMD|vW4NUJzHMoF#2rPDyvRw za2P#8gCAY`aGPXOnG8UeRy2Rt|K%|RO&@+f{KS?Cef4%*A91s73s)(NMAUYnP-qW@ z105laUuV^WUZhCx3z&QBH;#PFym;}(+WKB!z+6=ZFE;74nxN4m)WEB!Y^B0t5=)8! zbi?q_B3i?U|0WDFQ1Tv{w?d^yJJZ_D_pAJu&i;9ZMs+>ku5xRsB zEotH!LT&wNmC<4Cg?%09pbhr0F=)5j?U`(EBoc8+is4PSzMJgg6DRr>9?xQtD-<1Gw_B(&_~A`K(2MA+R348F$QoVv!0%0r{M7-9UT^rHxzyOr!of|D%9iEQnzbRpI1LHP=2~dZwncH zI;%c6*J@4ErKfBC293}ywa6npMI_S7lZ#vx6%}<2SK4mh!=V?fj=EXHBCZ3)yIC_g zb>F$od+#FXx{8VlvB<=0<;j&^sZ<(Lhlmyo215>q)9G}26b`db1-k0fUw`UGtT;%lGc?-F$Ea zXzUIXp%47x6@b2T2thYD*VQ$e1OmQHEakGHand`p1>Fmdx8$L z%BI)rdqD$3pu^({hdmynz~~8*%>$an67J~e=#a}z5k8N{6SVU9jg5VMZI`d!{RW4A z*ln@o80f2)+pae@Hg@p^yof+x5=-bO0Dg2imbiJW^Em?^vr6T2I)WY#2tSB_&;vSh zmCx)@1RQ4AG~g$;+wE?*8w6VFRVu||nH)X_?Cyd0^6q}K`CJqcTPg+o}O+2ud7iclJf*6js(2|$j=AJ*>G&k%>_!;Is;CfRj-F)@Bu!j(+6i9 zdYvz9G^2~vKplR+!H}oP6AJB_Y&KUa;fQ5?K`XDPtEXGg-w&Wyor}EzNnbXlZa|`I zn^Dj|-n@9Ze^@Xi=x%JRt8=+r;)o!k5#yVGL4mfo=Wzo!5bHJfR%qY^tweJ$P+ACAE z@=O$RcmJ(hw*mAUb6}*`Po=LSV^`H#e|YRM#{KD)OBcL572Q`R@r>%h#)ZKodm zu>ZrW$V{T4LDauum9Hl`?s%My?EjHx0lXrt3$>V}3vvB)GKUH}7bWrWuO9|*t=)%0|~#^^B=WD|=izcAp@WTlhDLc6qs zr?5!XYN^tq1jzF_Vo~RGwKpxVt{wy;;q&D#348_2c6%ed4z*pAf!>c~8QdVYFzo4mp|L~b&niI4XLnl^ zybh@vjDi-6QYw*fT;fjt(9o^6FRvXtcJadAL!0j$JVHD;JApkoOQ4=n#(&!PQuNb4 zX!=7w?fZB+z!r6O%1wNbkL=9MOg3BE!3%ioFx**LevMaQH)NrQQ5^xh!JqGkLG~JX z5sMo9dbnPTg~Q=+T(Zc}P=6a?T5|KyN%%$%Ib_Y+zJBVm4f9Z-qh3+s&~&I7k0^H? zK6363dPGUsOH_+GWg<}|A_8+h_^A zprxg=QzVu}OiH8?`0l!c7g4%}!n{18TcLzom0pF^4rX+2PlPA;y4eU>YLSUuO5#x; z{_b<#?oFRj9#ikH9p$;|v)E&1BGhpn2JhO$aG6*Rsa+yD-{hj7i-T}?sKK(&!&3`| zLMhMVw!5VrMlkGyUkQAxZ;^?_Vu0MzQoS5|82rZWO-Q-VYICPh_*~51{o=l zY$+@xv6Uu)BGSqeM8LQwvq-sIH6m8J;B-GCs;=I%JjO9&?k>y=aP`s^E2ogm)0hwR z;}|$V>P0aI4*Pa3qzoJ=Y!Gd^2uNEj;T9t7!a_FJD;Ko3wkj-MuSLd@a-|%xNbKTp zIAWQCK!#2TA5z{VOeX}wb#@WK9N)kJ_p2de9nUHQ`Yy$0WGBD|(%w>t8k;Runp$N_ zsniQP3b9BmM%^+`U=q2iaTl0XgbNIAl|eYp##&{J=fk}V`QS0ZjzO1Kh{OeB**VuUOvk#~Mb+5|y!mcvJG%-T+y zyOHi5DGAK+oA7^O#ah-(O9a#@$waU*`;g za&b8G!BD~%n8ekwVy1jlC}YbNGhQTJlqu$)KgP6wc|6)x48B}Nen!Y>XEkNCgN&{w zA!(mnvi~De+AWOe8zCBK}I{! zK^dJI##+`yxdUNOS0VQJ$rGVQXiKqeaV?FOx3u(`OeQ|hBzD1Vzv5DHtJo#(5?4Uv zF4{o=7c(?aiT2N%GG*eT@yTOT&`IaCSaAU8Xip<*LxG@XnnxWyb8a7L9<`1hY?_(? zHlNSqH^PTrrKO@)k*KTDRZ&qP?n3SX_+Ua>)le=>Ybb-Ui4!L$Cnux*M$8%r3p8}R zLOzPo7H&y|P1hpAri<`AJ_>ERaB1)VaO>7~fZf;G!{_n3MDQVCV{2nqm#D6;u8}X` zH#Ri1EN9qs?T)tTTJY)zE1oAk(a?qxZD+(Zyh4U(F=lyBkkn(KQ**2#+NRGI64-B@ zTmlVY+5vT655HT$Z>$5XtzG=C#^%Nz0k5mC0lFQ4Fdtt6XGVnm66P$n`UA>+j@WG^ znSUPRJ;(Yj(-AF(nsUpdd=%fOuo-Sa?d?rXSNi&Tdb)W%jm^zHt=;^_#x8-Nr?Ic$ zN)woXQZpmW=qlP(k+hyeMvx3+F+$L|UtV-F($sNG{vKc%`38}Z$mFjN?KLzu_O$Z4 zdw8wgJ$-%9n~*g5BLK;!%p$=Cc?Z86Enl%kIDQCrgJ6hN!(8{mO6>H zVI^HyX=$H};{w^%cIEo@>)iq{`RMMsejPKe#0HZXF(czj%-VDs%^Yn3NxE$^T_Bm% z2@F?LByBj@K#Rg%3rSb0lY0s8k~Toy+A8QC#wy-?S%iYvd^AU$g!p+Ku%o z+{JkUZpRA!SbtC2PBUz8VVAq^&gMf)_Fp`9?egWe;r{#m!^rk_KeW9)hg_DTW=5Fp zZM4NOgZWv~%^5lj(+0!wsi!eKk;gNzNrT}V#9qTb%wYK1<@?v#x9-FZhT+NtAjdHn zPLAVwi)n*ldP}+@k4DX~7^XB5ZRJcEHW60NpL}xflbw%h<($m$M8<5h<1>@~(<4wb zg&hz3-DM01c*->U`|t05|NY-+(`?enYJIFl?|6pQH*Qcq0&|qp_v1|GdfYafw)&1| zPfqPMoS#`a>&(#y55D^Po3Fq6D$1UGc3d-Lrq4ISdpbUIw5NJ}YSL6a2Ag3-O;$CS zd1BU%?MF}j_~7XFI7aYq$1#GBvakR7LzrXS>#-heUiFLhboGl}(FHpe(8$R#Z9GlYbBUsUW~90~j7mQkHc^Cz+9E+4+hqQ& zCAPWX1`L*cH1dRpm(IbNiOM-6mvo6Koii<}+D}ZmAJ~xqZCB13soW2<+Rub1@B%c< zx;c->ma(F16vd~G4mJ~; z>z_uU{#o2YVF;Vz1|tqpD2&{+9I1p=O3GM_I0iYY@K)R+XRx3g>!0FMQ^JlZycJz} z3vrXh!qBE=oMK@wp<-c-T49W0VI!3|i^EKoWgDfE9HYcpbd563lu_f@Byt9d zP|`IW$r_1Cs0`(ZMI9$#z){5!AFVp#bBs!qBb0Sy20Ny(BLc z`bi_>(ul6;7*+FgB#Feuj;>RR3YCtFU#OH(9X3v#(x+mp!j4j>bd>6Ws~ls*qau%3Qkpe-4eTdlu>aTVN&j}ldeT%GY!&2 + exit 1 +} + +read_opt() { + local key="$1" + jq -er --arg k "$key" '.[$k]' "$OPTIONS_JSON" 2>/dev/null || true +} + +normalize_path() { + local raw="$1" + if command -v realpath >/dev/null 2>&1; then + realpath -m "$raw" + return + fi + + case "$raw" in + /*) printf '%s\n' "$raw" ;; + *) printf '/%s\n' "$raw" ;; + esac +} + +is_allowed_path() { + local resolved="$1" + case "$resolved" in + /share|/share/*|/media|/media/*|/config|/config/*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +require_mapped_path() { + local label="$1" + local raw="$2" + local resolved + + resolved="$(normalize_path "$raw")" + if ! is_allowed_path "$resolved"; then + die "${label} '${raw}' resolves to '${resolved}', which is outside /share, /media, and /config" + fi + + printf '%s\n' "$resolved" +} + +ensure_dir() { + local dir="$1" + mkdir -p "$dir" +} + +ensure_existing_or_create() { + local label="$1" + local dir="$2" + + if [[ -d "$dir" ]]; then + return + fi + + if mkdir -p "$dir" 2>/dev/null; then + return + fi + + die "${label} '${dir}' does not exist and could not be created. Create it on the host or choose a writable path under /config." +} + +chown_recursive_if_writable() { + local owner="$1" + local path="$2" + + if [[ ! -e "$path" ]]; then + log "Skipping ownership update for ${path} (missing path)" + return + fi + + if [[ -w "$path" ]]; then + chown -R "$owner" "$path" + return + fi + + log "Skipping ownership update for ${path} (read-only mapping)" +} + +generate_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex 64 + return + fi + + head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n' +} + +start_manyfold() { + if [[ -x /usr/src/app/bin/docker-entrypoint.sh ]]; then + log "Starting Manyfold via /usr/src/app/bin/docker-entrypoint.sh foreman start" + cd /usr/src/app + exec ./bin/docker-entrypoint.sh foreman start + fi + + if [[ -x /app/bin/docker-entrypoint.sh ]]; then + log "Starting Manyfold via /app/bin/docker-entrypoint.sh foreman start" + cd /app + exec ./bin/docker-entrypoint.sh foreman start + fi + + local candidate + for candidate in \ + /usr/local/bin/docker-entrypoint.sh \ + /usr/local/bin/docker-entrypoint \ + /docker-entrypoint.sh \ + /entrypoint.sh + do + if [[ -x "$candidate" ]]; then + log "Starting Manyfold via ${candidate}" + if [[ "$candidate" == *docker-entrypoint* ]]; then + exec "$candidate" foreman start + fi + exec "$candidate" + fi + done + + if command -v docker-entrypoint >/dev/null 2>&1; then + log "Starting Manyfold via docker-entrypoint" + exec docker-entrypoint foreman start + fi + + if [[ -d /usr/src/app ]]; then + cd /usr/src/app + elif [[ -d /app ]]; then + cd /app + fi + + if command -v bundle >/dev/null 2>&1; then + log "Starting Manyfold via rails server fallback" + exec bundle exec rails server -b 0.0.0.0 -p 3214 + fi + + die "Could not find a known Manyfold entrypoint" +} + +[[ -f "$OPTIONS_JSON" ]] || die "Missing options file at ${OPTIONS_JSON}" + +PUID="$(read_opt puid)"; PUID="${PUID:-1000}" +PGID="$(read_opt pgid)"; PGID="${PGID:-1000}" +MULTIUSER="$(read_opt multiuser)"; MULTIUSER="${MULTIUSER:-true}" +LIBRARY_PATH_RAW="$(read_opt library_path)"; LIBRARY_PATH_RAW="${LIBRARY_PATH_RAW:-$DEFAULT_LIBRARY_PATH}" +THUMBNAILS_PATH_RAW="$(read_opt thumbnails_path)"; THUMBNAILS_PATH_RAW="${THUMBNAILS_PATH_RAW:-$DEFAULT_THUMBNAILS_PATH}" +LOG_LEVEL="$(read_opt log_level)"; LOG_LEVEL="${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" +WEB_CONCURRENCY="$(read_opt web_concurrency)"; WEB_CONCURRENCY="${WEB_CONCURRENCY:-$DEFAULT_WEB_CONCURRENCY}" +RAILS_MAX_THREADS="$(read_opt rails_max_threads)"; RAILS_MAX_THREADS="${RAILS_MAX_THREADS:-$DEFAULT_RAILS_MAX_THREADS}" +DEFAULT_WORKER_CONCURRENCY="$(read_opt default_worker_concurrency)"; DEFAULT_WORKER_CONCURRENCY="${DEFAULT_WORKER_CONCURRENCY:-$DEFAULT_DEFAULT_WORKER_CONCURRENCY}" +PERFORMANCE_WORKER_CONCURRENCY="$(read_opt performance_worker_concurrency)"; PERFORMANCE_WORKER_CONCURRENCY="${PERFORMANCE_WORKER_CONCURRENCY:-$DEFAULT_PERFORMANCE_WORKER_CONCURRENCY}" +MAX_FILE_UPLOAD_SIZE="$(read_opt max_file_upload_size)"; MAX_FILE_UPLOAD_SIZE="${MAX_FILE_UPLOAD_SIZE:-$DEFAULT_MAX_FILE_UPLOAD_SIZE}" +MAX_FILE_EXTRACT_SIZE="$(read_opt max_file_extract_size)"; MAX_FILE_EXTRACT_SIZE="${MAX_FILE_EXTRACT_SIZE:-$DEFAULT_MAX_FILE_EXTRACT_SIZE}" +SECRET_KEY_BASE="$(read_opt secret_key_base)"; SECRET_KEY_BASE="${SECRET_KEY_BASE:-}" + +[[ "$PUID" =~ ^[0-9]+$ ]] || die "puid must be a non-negative integer" +[[ "$PGID" =~ ^[0-9]+$ ]] || die "pgid must be a non-negative integer" +[[ "$WEB_CONCURRENCY" =~ ^[1-9][0-9]*$ ]] || die "web_concurrency must be a positive integer" +[[ "$RAILS_MAX_THREADS" =~ ^[1-9][0-9]*$ ]] || die "rails_max_threads must be a positive integer" +[[ "$DEFAULT_WORKER_CONCURRENCY" =~ ^[1-9][0-9]*$ ]] || die "default_worker_concurrency must be a positive integer" +[[ "$PERFORMANCE_WORKER_CONCURRENCY" =~ ^[1-9][0-9]*$ ]] || die "performance_worker_concurrency must be a positive integer" +[[ "$MAX_FILE_UPLOAD_SIZE" =~ ^[1-9][0-9]*$ ]] || die "max_file_upload_size must be a positive integer (bytes)" +[[ "$MAX_FILE_EXTRACT_SIZE" =~ ^[1-9][0-9]*$ ]] || die "max_file_extract_size must be a positive integer (bytes)" + +LIBRARY_PATH="$(require_mapped_path "library_path" "$LIBRARY_PATH_RAW")" +THUMBNAILS_PATH="$(require_mapped_path "thumbnails_path" "$THUMBNAILS_PATH_RAW")" + +case "$THUMBNAILS_PATH" in + /config|/config/*) ;; + *) die "thumbnails_path must resolve under /config for persistence" ;; +esac + +ensure_dir "$CONFIG_DIR" +ensure_dir "$DEFAULT_THUMBNAILS_PATH" +ensure_existing_or_create "library_path" "$LIBRARY_PATH" +ensure_dir "$THUMBNAILS_PATH" +[[ -r "$LIBRARY_PATH" ]] || die "library_path '${LIBRARY_PATH}' is not readable" + +if [[ -z "$SECRET_KEY_BASE" ]]; then + if [[ -s "$SECRET_FILE" ]]; then + SECRET_KEY_BASE="$(cat "$SECRET_FILE")" + log "Loaded SECRET_KEY_BASE from ${SECRET_FILE}" + else + SECRET_KEY_BASE="$(generate_secret)" + printf '%s' "$SECRET_KEY_BASE" > "$SECRET_FILE" + chmod 600 "$SECRET_FILE" + log "Generated and stored SECRET_KEY_BASE at ${SECRET_FILE}" + fi +else + printf '%s' "$SECRET_KEY_BASE" > "$SECRET_FILE" + chmod 600 "$SECRET_FILE" + log "Saved provided SECRET_KEY_BASE to ${SECRET_FILE}" +fi + +export SECRET_KEY_BASE +export PUID +export PGID +export MULTIUSER +export MANYFOLD_MULTIUSER="$MULTIUSER" +export MANYFOLD_LIBRARY_PATH="$LIBRARY_PATH" +export MANYFOLD_THUMBNAILS_PATH="$THUMBNAILS_PATH" +export RAILS_LOG_LEVEL="$LOG_LEVEL" +export MANYFOLD_LOG_LEVEL="$LOG_LEVEL" +export WEB_CONCURRENCY +export RAILS_MAX_THREADS +export DEFAULT_WORKER_CONCURRENCY +export PERFORMANCE_WORKER_CONCURRENCY +export MAX_FILE_UPLOAD_SIZE +export MAX_FILE_EXTRACT_SIZE +export PORT="3214" + +chown_recursive_if_writable "$PUID:$PGID" "$CONFIG_DIR" +chown_recursive_if_writable "$PUID:$PGID" "$DEFAULT_THUMBNAILS_PATH" +chown_recursive_if_writable "$PUID:$PGID" "$LIBRARY_PATH" +chown_recursive_if_writable "$PUID:$PGID" "$THUMBNAILS_PATH" + +log "Configuration summary:" +log " library_path=${LIBRARY_PATH}" +log " thumbnails_path=${THUMBNAILS_PATH}" +log " multiuser=${MULTIUSER}" +log " puid:pgid=${PUID}:${PGID}" +log " web_concurrency=${WEB_CONCURRENCY}" +log " rails_max_threads=${RAILS_MAX_THREADS}" +log " default_worker_concurrency=${DEFAULT_WORKER_CONCURRENCY}" +log " performance_worker_concurrency=${PERFORMANCE_WORKER_CONCURRENCY}" +log " max_file_upload_size=${MAX_FILE_UPLOAD_SIZE}" +log " max_file_extract_size=${MAX_FILE_EXTRACT_SIZE}" + +start_manyfold diff --git a/manyfold/translations/en.yaml b/manyfold/translations/en.yaml new file mode 100644 index 000000000..e5c2069f6 --- /dev/null +++ b/manyfold/translations/en.yaml @@ -0,0 +1,40 @@ +configuration: + secret_key_base: + name: Secret key base + description: Leave blank to auto-generate and persist at /config/secret_key_base. + puid: + name: PUID + description: User ID for file ownership on writable mapped volumes. + pgid: + name: PGID + description: Group ID for file ownership on writable mapped volumes. + multiuser: + name: Multiuser mode + description: Enable or disable Manyfold multiuser login. + library_path: + name: Library path + description: Folder scanned/indexed by Manyfold. Must be under /share, /media, or /config. + thumbnails_path: + name: Thumbnails path + description: Path for thumbnails/index artifacts. Must resolve under /config. + log_level: + name: Log level + description: Rails log verbosity. + web_concurrency: + name: Web workers + description: Puma worker process count (WEB_CONCURRENCY). Lower this on small servers. + rails_max_threads: + name: Web max threads + description: Max threads per Puma worker (RAILS_MAX_THREADS). Lower values reduce memory use. + default_worker_concurrency: + name: Default worker concurrency + description: Sidekiq concurrency for the default worker queue. + performance_worker_concurrency: + name: Performance worker concurrency + description: Sidekiq concurrency for the performance queue. + max_file_upload_size: + name: Max upload size (bytes) + description: Upper limit for uploaded archive size (MAX_FILE_UPLOAD_SIZE). + max_file_extract_size: + name: Max extract size (bytes) + description: Upper limit for extracted archive size (MAX_FILE_EXTRACT_SIZE).