Compare commits

..

20 Commits

Author SHA1 Message Date
borgal
c80f1723e0 config angepasst 2023-09-07 20:39:09 +02:00
borgal
87eb4018d9 config angepasst 2023-09-07 20:38:40 +02:00
borgal
d2de6c27bb config angepasst 2023-09-07 20:38:25 +02:00
borgal
2d6bde89db port angepasst 2023-09-07 20:37:49 +02:00
borgal
bd8715b2f4 Config angepasst 2023-09-07 20:37:09 +02:00
borgal
e32a5b206e Config angepasst 2023-09-07 20:36:51 +02:00
borgal
2c0c6c3384 erneut hinzugefügt 2023-09-07 15:36:04 +02:00
borgal
e8eb471658 Merge branch 'master' of http://git.borgal.de/Borgal/docker 2023-09-07 15:31:35 +02:00
borgal
c86744ec3b entfernen 2023-09-07 15:31:05 +02:00
Borgal
5bc35e7fee onlyoffice/docker-compose.yml gelöscht 2023-09-07 15:29:50 +02:00
borgal
46d106cb38 PW entfernt 2023-09-07 15:24:42 +02:00
borgal
d0bee4a21c Ports angepasst 2023-05-31 21:55:00 +02:00
borgal
6405b2864e Ports angepasst 2023-05-31 21:54:34 +02:00
borgal
0d4065c89f Ports angepasst 2023-05-31 21:54:17 +02:00
borgal
336db8c003 Ports angepasst 2023-05-31 21:54:07 +02:00
borgal
807495c460 Ports angepasst 2023-05-31 21:53:34 +02:00
borgal
3f392c4674 Ports angepasst 2023-05-31 21:53:23 +02:00
borgal
ccd14069d6 Ports angepasst 2023-05-31 21:53:11 +02:00
borgal
569d5f25ea owncloud hinzugefügt 2023-05-31 21:52:12 +02:00
borgal
b9c7b02155 onlyoffice hinzugefügt 2023-05-31 21:51:51 +02:00
118 changed files with 591 additions and 3679 deletions

View File

@@ -1,17 +1,13 @@
<H1>Docker-Compose Files Borgals HomeLab</H1> ## Docker-Compose Files Borgals HomeLab
<p></p>
<p></p>
<img src="https://tcude.net/content/images/size/w2000/2022/01/MainImage-2.jpeg" alt="Docker-Compose" width="400" />
<p></p>
<p></p>
<H2>Environment Variablen</H2>
- **Benutzernamen** und **Passwörter**, sowie weitere Variablen sind in einer .env Datei ausgelagert ### Environment Variablen
- die **.env.sample** muss jeweils in **.env** umbenannt werden und die Variablen darin auf deine Bedürfnisse angepasst werden
---- - Benutzernamen und Passwörter, sowie weitere Variablen sind in einer .env Datei ausgelagert
- die .env.sample muss jeweils in .env umbenannt werden und die Variablen darin auf deine Bedürfnisse angepasst werden
<H2>Reverse Proxy</H2>
- als **Reverse Proxy** setze ich den Nginx Proxy Manager ein, daher sind keine Labels für Träfik vorhanden ### Reverse Proxy
- als Reverse Proxy setze ich den Nginx Proxy Manager ein, daher sind keine Labels für Träfik vorhanden

View File

@@ -18,8 +18,6 @@ dns:
port: 53 port: 53
anonymize_client_ip: false anonymize_client_ip: false
ratelimit: 0 ratelimit: 0
ratelimit_subnet_len_ipv4: 24
ratelimit_subnet_len_ipv6: 56
ratelimit_whitelist: [] ratelimit_whitelist: []
refuse_any: true refuse_any: true
upstream_dns: upstream_dns:
@@ -39,7 +37,8 @@ dns:
- 8.8.8.8 - 8.8.8.8
- 8.8.4.4 - 8.8.4.4
fallback_dns: [] fallback_dns: []
upstream_mode: load_balance all_servers: false
fastest_addr: false
fastest_timeout: 1s fastest_timeout: 1s
allowed_clients: [] allowed_clients: []
disallowed_clients: [] disallowed_clients: []
@@ -68,14 +67,12 @@ dns:
bootstrap_prefer_ipv6: false bootstrap_prefer_ipv6: false
upstream_timeout: 10s upstream_timeout: 10s
private_networks: [] private_networks: []
use_private_ptr_resolvers: false use_private_ptr_resolvers: true
local_ptr_upstreams: [] local_ptr_upstreams: []
use_dns64: false use_dns64: false
dns64_prefixes: [] dns64_prefixes: []
serve_http3: false serve_http3: false
use_http3_upstreams: false use_http3_upstreams: false
serve_plain_dns: true
hostsfile_enabled: true
tls: tls:
enabled: false enabled: false
server_name: "" server_name: ""
@@ -92,14 +89,12 @@ tls:
private_key_path: "" private_key_path: ""
strict_sni_check: false strict_sni_check: false
querylog: querylog:
dir_path: ""
ignored: [] ignored: []
interval: 24h interval: 24h
size_memory: 1000 size_memory: 1000
enabled: true enabled: true
file_enabled: true file_enabled: true
statistics: statistics:
dir_path: ""
ignored: [] ignored: []
interval: 24h interval: 24h
enabled: true enabled: true
@@ -185,15 +180,16 @@ user_rules:
- /^r[0-9]-*sn-[a-z0-9]*-[0-9a-z]{4}\.googlevideo\.com/ - /^r[0-9]-*sn-[a-z0-9]*-[0-9a-z]{4}\.googlevideo\.com/
- /^r[0-9]{1,2}-*sn-[a-z0-9]*-[0-9a-z]{4}.googlevideo.com/ - /^r[0-9]{1,2}-*sn-[a-z0-9]*-[0-9a-z]{4}.googlevideo.com/
- /^rr[0-9]-*sn-[a-z0-9]*-[0-9a-z]{4}\.googlevideo\.com/ - /^rr[0-9]-*sn-[a-z0-9]*-[0-9a-z]{4}\.googlevideo\.com/
- '##'
- '# PandaSecurity'
- '##'
- '||eventtrack.pandasecurity.com^$important'
- '#####'
- '# sonstige sperren'
- '#####'
- '@@||ad.doubleclick.net^$important' - '@@||ad.doubleclick.net^$important'
- '@@||app-measurement.com^$important' - '@@||app-measurement.com^$important'
- '!@@||www.googletagmanager.com^$important'
- '!@@||www.google-analytics.com^$important'
- '!@@||as.bild.de^$important'
- '!@@||axelspringerse.demdex.net^$important'
- '!@@||adnxs-simple.com^$important'
- '!@@||a.hstrck.com^$important'
- '!@@||cdn.cxense.com^$important'
- '!@@||cm.everesttech.net^$important'
- '@@||benefits.sovendus.com^$important' - '@@||benefits.sovendus.com^$important'
- '@@||fhdi3gj7.r.us-east-1.awstrack.me^$important' - '@@||fhdi3gj7.r.us-east-1.awstrack.me^$important'
- '@@||t.notifications.groupe-pvcp.com^$important' - '@@||t.notifications.groupe-pvcp.com^$important'
@@ -208,23 +204,8 @@ user_rules:
- '@@||as.bild.de^$important' - '@@||as.bild.de^$important'
- '@@||click.cptrack.de^$important' - '@@||click.cptrack.de^$important'
- '@@||lavieenrose.com^$important' - '@@||lavieenrose.com^$important'
- '@@||fast.skydeutschland.demdex.net^$client=''ROG''' - '||eventtrack.pandasecurity.com^$important'
- '@@||email.golfpost.de^$important'
- '@@||adservice.google.com^$important'
- '@@||rd.bizrate.com^$important'
- '@@||telemetry-in.battle.net^$important' - '@@||telemetry-in.battle.net^$important'
- '#'
- '## Sperre für Luminar '
- '#'
- '||auth.macphun.com^$important'
- '||luminar3win.update.skylum.com^$important'
- '||luminar3.s3-accelerate.amazonaws.com^$important'
- '@@||api.segment.io^$important'
- '@@||googlevideo.com^$important'
- '@@||invite.journiapp.com^$important'
- '@@||googleads.g.doubleclick.net^$important'
- '@@||event-collector.prd.data.s.joyn.de^$important'
- '@@||vendor-list.consensu.org^$important'
- "" - ""
dhcp: dhcp:
enabled: false enabled: false
@@ -298,9 +279,6 @@ clients:
- 192.168.0.51 - 192.168.0.51
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-71e0-9a76-f7f93da1e64d
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false use_global_settings: false
filtering_enabled: true filtering_enabled: true
parental_enabled: false parental_enabled: false
@@ -325,9 +303,6 @@ clients:
- 192.168.0.56 - 192.168.0.56
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-7d6c-a1d4-2f005d7c17ab
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: true use_global_settings: true
filtering_enabled: false filtering_enabled: false
parental_enabled: false parental_enabled: false
@@ -352,9 +327,6 @@ clients:
- 192.168.0.1 - 192.168.0.1
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-7f8d-95fe-fcfe6d363a29
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false use_global_settings: false
filtering_enabled: true filtering_enabled: true
parental_enabled: false parental_enabled: false
@@ -379,9 +351,6 @@ clients:
- 192.168.0.59 - 192.168.0.59
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-7739-bbc2-bf9b979f9913
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false use_global_settings: false
filtering_enabled: true filtering_enabled: true
parental_enabled: false parental_enabled: false
@@ -406,9 +375,6 @@ clients:
- 192.168.0.98 - 192.168.0.98
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-741f-9824-85cdb1315521
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false use_global_settings: false
filtering_enabled: true filtering_enabled: true
parental_enabled: false parental_enabled: false
@@ -433,9 +399,6 @@ clients:
- 192.168.0.58 - 192.168.0.58
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-70b1-8f0b-e7be87a33423
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false use_global_settings: false
filtering_enabled: true filtering_enabled: true
parental_enabled: false parental_enabled: false
@@ -460,63 +423,6 @@ clients:
- 192.168.0.42 - 192.168.0.42
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-7e3c-a8e5-2d70f8580abb
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false
filtering_enabled: true
parental_enabled: false
safebrowsing_enabled: true
use_global_blocked_services: true
ignore_querylog: false
ignore_statistics: false
- safe_search:
enabled: false
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
blocked_services:
schedule:
time_zone: Europe/Berlin
ids: []
name: S20+ Lars
ids:
- 192.168.0.60
tags: []
upstreams: []
uid: 018d7fa4-2a1d-7acf-aa55-c000293947bc
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false
filtering_enabled: true
parental_enabled: false
safebrowsing_enabled: true
use_global_blocked_services: true
ignore_querylog: false
ignore_statistics: false
- safe_search:
enabled: false
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
blocked_services:
schedule:
time_zone: Europe/Berlin
ids: []
name: S7-Lars
ids:
- 192.168.0.62
tags: []
upstreams: []
uid: 018d7fa4-2a1d-7fb5-a762-4f4599510a9d
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: false use_global_settings: false
filtering_enabled: true filtering_enabled: true
parental_enabled: false parental_enabled: false
@@ -541,9 +447,6 @@ clients:
- 192.168.0.46 - 192.168.0.46
tags: [] tags: []
upstreams: [] upstreams: []
uid: 018d7fa4-2a1d-729a-a02b-bb7bd6c6340f
upstreams_cache_size: 0
upstreams_cache_enabled: false
use_global_settings: true use_global_settings: true
filtering_enabled: false filtering_enabled: false
parental_enabled: false parental_enabled: false
@@ -563,4 +466,4 @@ os:
group: "" group: ""
user: "" user: ""
rlimit_nofile: 0 rlimit_nofile: 0
schema_version: 28 schema_version: 27

View File

@@ -1,14 +1,12 @@
version: '3.7'
services: services:
adguardhome: adguardhome:
container_name: adguardhome container_name: adguardhome
hostname: adguardhome
image: adguard/adguardhome:latest image: adguard/adguardhome:latest
volumes: volumes:
- ./work:/opt/adguardhome/work - ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf - ./conf:/opt/adguardhome/conf
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
cap_add:
- NET_ADMIN
ports: ports:
- 53:53/tcp - 53:53/tcp
- 53:53/udp - 53:53/udp

View File

@@ -1,51 +1,5 @@
<h2>AdGuard</H2> ## AdGuard
<p></p>
<p></p> ![](https://res.cloudinary.com/canonical/image/fetch/f_auto,q_auto,fl_sanitize,c_fill,w_720/https://ubuntu.com/wp-content/uploads/10b2/adguard-compressor.png)
<img src="https://ubuntu.com/wp-content/uploads/10b2/adguard-compressor.png" alt="Adguard" width="200" />
<p></p> AdGuard Home ist eine netzwerkweite Software zum Sperren von Werbung und Tracking. Nachdem Sie es eingerichtet haben, deckt es ALLE Ihre Heimgeräte ab, und Sie brauchen dafür keine clientseitige Software. Mit dem Aufstieg von „Internet der Dinge” und vernetzten Geräten wird es immer wichtiger, Ihr gesamtes Netzwerk zu kontrollieren.
<p></p>
<p><b>AdGuard Home</b> ist eine netzwerkweite Software zum Sperren von Werbung und Tracking. Nachdem Sie es eingerichtet haben, deckt es ALLE Ihre Heimgeräte ab, und Sie brauchen dafür keine clientseitige Software. Mit dem Aufstieg von „Internet der Dinge” und vernetzten Geräten wird es immer wichtiger, Ihr gesamtes Netzwerk zu kontrollieren.
</p>
<p></p>
<a href="https://adguard.com/de/adguard-home/overview.html/" target="_blank">Adguard Home</a>
<p></p>
<h3>HowTo</h3>
<hr>
<p></p>
<p>Upstream DNS Server</p>
<pre>[/fritz.box/]192.168.0.1
[/168.192.in-addr.arpa/]192.168.0.1</code></pre>
<p></p>
<p></p>
<h4>Port 53 Anpassung</h4>
<p></p>
<pre>systemctl stop systemd-resolved
systemctl disable systemd-resolved</pre>
<p></p>
<pre>nano /etc/resolv.conf</pre>
<p></p>
<pre># This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients to the
# internal DNS stub resolver of systemd-resolved. This file lists all
# configured search domains.
#
# Run "resolvectl status" to see details about the uplink DNS servers
# currently in use.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
#
# nameserver 127.0.0.53 entfert für Port 53 von AdGuard Home
#
nameserver 127.0.0.1
options edns0 trust-ad
search .</pre>

View File

@@ -1,8 +0,0 @@
# Admin-Token für die Anmeldung der Admin-Page
# Anmeldung über https: bitwarden.MyDomain.de/admin
# Hier ist das Passwort anzugeben, womit der Token erzeugt wurde
# Den Token kann man z.B. über docker exec -it bitwarden ./vaultwarden hash erzeugen
# WICHTIG - Wird die Admin Page nicht benötigt, dann sollte man die Zeile ADMIN_TOKEN in der docker-compose Datei mit einem # auskommentieren
ADMIN_TOKEN='ChangeMe'

View File

@@ -1,10 +1,8 @@
version: "2"
services: services:
bitwarden: bitwarden:
image: vaultwarden/server:latest-alpine image: vaultwarden/server:latest
container_name: bitwarden container_name: bitwarden
environment:
- TZ=Europe/Berlin
# - ADMIN_TOKEN=${ADMIN_TOKEN} # Wird die Admin-Page nicht gebraucht, bitte auskommentieren
volumes: volumes:
- /opt/bitwarden/bwdata/:/data/ - /opt/bitwarden/bwdata/:/data/
ports: ports:

View File

@@ -1,16 +1,11 @@
<h2>Bitwarden/Vaultwarden</H2> ## Bitwarden
<p></p>
<p></p> ![](https://i.pcmag.com/imagery/reviews/05JPSXpKxx9c5oL8wwZMAkX-27..1622837895.png)
<img src="https://i.pcmag.com/imagery/reviews/05JPSXpKxx9c5oL8wwZMAkX-27..1622837895.png" alt="Bitwarden" width="200" />
<p></p> Bitwarden ist der einfachste und sicherste Weg all deine Logins und Passwörter zu speichern, während du sie zwischen all deinen Geräten synchronisierst.
<p></p>
<b>Bitwarden/Vaultwarden</b> ist der einfachste und sicherste Weg all deine Logins und Passwörter zu speichern, während du sie zwischen all deinen Geräten synchronisierst.
Passwort-Diebstahl ist ein echtes Problem. Die Webseiten und Apps, welche du verwendest, werden jeden Tag angegriffen und oftmals werden deine Passwörter dabei gestohlen. Wenn das gleiche Passwort bei mehreren Apps und Websites verwendet wird, können Hacker sich damit ganz einfach auch in deine E-Mails oder deinen Bank-Account einloggen. Passwort-Diebstahl ist ein echtes Problem. Die Webseiten und Apps, welche du verwendest, werden jeden Tag angegriffen und oftmals werden deine Passwörter dabei gestohlen. Wenn das gleiche Passwort bei mehreren Apps und Websites verwendet wird, können Hacker sich damit ganz einfach auch in deine E-Mails oder deinen Bank-Account einloggen.
Sicherheitsexperten empfehlen daher, dass du für jeden Account, den du erstellst, ein anderes, zufällig generiertes Passwort verwendest. Aber wie sollst du dir all diese Passwörter merken? bitwarden hilft dir dabei, deine Passwörter zu erstellen, speichern und zu verwalten. Sicherheitsexperten empfehlen daher, dass du für jeden Account, den du erstellst, ein anderes, zufällig generiertes Passwort verwendest. Aber wie sollst du dir all diese Passwörter merken? bitwarden hilft dir dabei, deine Passwörter zu erstellen, speichern und zu verwalten.
Bitwarden speichert all deine Logins in einem verschlüsselten Tresor, der mit allen Geräten synchronisiert wird. Da er komplett verschlüsselt ist, bevor er überhaupt dein Gerät verlässt, hast nur du Zugriff auf deine Daten. Nicht einmal Bitwarden kann deine Daten lesen, selbst, wenn wir es wollten. Deine Daten sind mit einer AES-256-Bit-Verschlüsselung, sowie Salted Hashing und PBKDR2 SHA-256 abgesichert. Bitwarden speichert all deine Logins in einem verschlüsselten Tresor, der mit allen Geräten synchronisiert wird. Da er komplett verschlüsselt ist, bevor er überhaupt dein Gerät verlässt, hast nur du Zugriff auf deine Daten. Nicht einmal Bitwarden kann deine Daten lesen, selbst, wenn wir es wollten. Deine Daten sind mit einer AES-256-Bit-Verschlüsselung, sowie Salted Hashing und PBKDR2 SHA-256 abgesichert.
<p></p>
<a href="https://hub.docker.com/r/vaultwarden/server/" target="_blank">Vaultwarden</a>
<p></p>

View File

@@ -1,10 +1 @@
<h2>Bookstack</H2> Beschreibung
<p></p>
<p></p>
<img src="https://project.borgal.de/images/logo/bookstack.png" alt="Bookstack" width="200" />
<p></p>
<p></p>
<b>Bookstack</b> ist eine benutzerfreundliche und flexible Plattform für das Content-Management. Mit seiner intuitiven Oberfläche können Benutzer leicht Texte, Bilder, Dateien und Code-Blöcke erstellen und organisieren.
<p></p>
<a href="https://www.bookstackapp.com/" target="_blank">Bookstack</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: "3"
services: services:
bookstack: bookstack:
image: ghcr.io/linuxserver/bookstack image: ghcr.io/linuxserver/bookstack
@@ -5,7 +6,6 @@ services:
environment: environment:
- APP_URL=https://${DOMAIN} - APP_URL=https://${DOMAIN}
- NGINX_ENABLE_FASTCGI_HTTPS=True - NGINX_ENABLE_FASTCGI_HTTPS=True
- TZ=Europe/Berlin
- DB_HOST=bookstack_db - DB_HOST=bookstack_db
- DB_USER=${DB_USER} - DB_USER=${DB_USER}
- DB_PASS=${DB_PASS} - DB_PASS=${DB_PASS}

8
cloud/.env.sample Normal file
View File

@@ -0,0 +1,8 @@
MYSQL_PASSWORD=changeMe
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_ROOT_PASSWORD=changeMe
NFS_SERVER=192.168.0.xxx # oder hostname oder Domain
PFAD=/Pfad/zum/Ordner

58
cloud/docker-compose.yml Normal file
View File

@@ -0,0 +1,58 @@
version: "3"
services:
cloud_db:
image: ghcr.io/linuxserver/mariadb
container_name: cloud_db
restart: always
volumes:
- ./database:/config
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
labels:
- "com.centurylinklabs.watchtower.enable=false"
cloud_redis:
image: redis:alpine
volumes:
- ./redis:/data
container_name: cloud_redis
restart: always
cloud:
image: ghcr.io/linuxserver/nextcloud
container_name: cloud
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
- MYSQL_HOST=cloud_db
- REDIS_HOST=cloud_redis
depends_on:
- cloud_db
- cloud_redis
volumes:
- ./config:/config
- cloud:/data
ports:
- 8100:443
labels:
- "com.centurylinklabs.watchtower.enable=false"
restart: unless-stopped
volumes:
cloud:
name: cloud
driver: local
driver_opts:
type: nfs
o: addr=${NFS_SERVER},rw
device: ":${PFAD}"

View File

@@ -1,23 +0,0 @@
<h2>Code-Server</H2>
<p></p>
<p></p>
<img src="https://project.borgal.de/images/logo/code-server.png" alt="Code-Server" width="200" />
<p></p>
<p></p>
<b>Code-Server</b> Visual Studio Code fungiert als portable Entwicklungsumgebung, um die Qualitätssicherung, Downloads und vieles mehr zu beschleunigen. Die optionale IntelliSense-Funktion führt wortbasierte Vervollständigungen für viele Programmiersprachen out-of-the-box durch, und noch mehr sind als Erweiterungen verfügbar. Verwenden Sie eine Reihe flexibler Erweiterungen, um neue Sprachen hinzuzufügen und eine Verbindung zu den Diensten herzustellen, die Sie bereits verwenden.
<p></p>
<a href="https://github.com/coder/code-server/" target="_blank">Code-Server</a>
<p></p>
<h3>HowTo</h3>
<hr>
<p></p>
<h3>Code-Server</h3>
<p>F&uuml;r den Code-Server muss im Nginx Proxy-Manager der Haken bei "Websockets Support" aktiviert werden</p>
<p></p>
<p><a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-visual-studio-code-for-php-projects" target="_blank">PHP Umgebung installieren</a></p>
<p><a href="https://marketplace.visualstudio.com/" target="_blank">VS-Code Erweiterungen</a></p>
<p></p>
<p>Ihre Benutzereinstellungen befinden sich in /opt/code-server/config/data/User</p>
<p>Sie k&ouml;nnen einfach die Dateien keybindings.json und settings.json in den entsprechenden Ordner auf Ihrem neuen Computer kopieren.</p>
<p>Ihre Erweiterungen befinden sich in /opt/code-server/config/extensions.<br>Die meisten Erweiterungen verwenden keine nativen Bindungen und sollten beim Kopieren ordnungsgem&auml;&szlig; funktionieren.<br>Sie k&ouml;nnen diejenigen, die dies nicht tun, manuell neu installieren.</p>
<p></p>

View File

@@ -1,3 +1,5 @@
version: "3"
services: services:
code-server: code-server:
image: ghcr.io/linuxserver/code-server image: ghcr.io/linuxserver/code-server
@@ -26,6 +28,6 @@ volumes:
name: www name: www
driver: local driver: local
driver_opts: driver_opts:
type: nfs4 type: nfs
o: addr=${NFS_SERVER},rw o: addr=${NFS_SERVER},rw
device: ":${PFAD}" device: ":${PFAD}"

View File

@@ -1,10 +0,0 @@
<h2>DOZZLE</H2>
<p></p>
<p></p>
<img src="https://developer.asustor.com/uploadIcons/0020_117518_1660879946_dozzle_256.png" alt="dozzle" width="200"/>
<p></p>
<p></p>
<b>Dozzle</b> ist ein Realtime Docker Container Monitor für Logging.
<p></p>
<a href="https://dozzle.dev/" target="_blank">Dozzle</a>
<p></p>

View File

@@ -1,9 +0,0 @@
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 9999:8080
restart: unless-stopped

View File

@@ -1,10 +0,0 @@
<h2>Draw.io</H2>
<p></p>
<p></p>
<img style="display: block; margin-left: auto; margin-right: auto;" src="https://cdn.worldvectorlogo.com/logos/draw-io.svg" alt="draw.io" width="200"/>
<p></p>
<p></p>
<b>Draw.io</b> ist ein Online-Diagrammsoftware zum Erstellen von Flussdiagrammen, Prozessdiagrammen und Organigrammen.
<p></p>
<a href="https://draw.io/" target="_blank">Draw.io</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: '3.5'
services: services:
drawio: drawio:
image: jgraph/drawio image: jgraph/drawio

View File

@@ -1,3 +1,2 @@
PASS=ChangeMe
NFS_SERVER=192.168.0.xxx # oder hostname oder Domain NFS_SERVER=192.168.0.xxx # oder hostname oder Domain
PFAD=/Pfad/zum/Ordner PFAD=/Pfad/zum/Ordner

View File

@@ -1,10 +0,0 @@
<h2>Duplicati</H2>
<p></p>
<p></p>
<img src="https://avatars.githubusercontent.com/u/8270231?s=280&v=4" alt="Duplicati" width="200" />
<p></p>
<p></p>
<b>Duplicati</b> ist eine Software, die verschlüsselte, komprimierte, inkrementelle Datensicherungen erstellt und diese auf Netzwerklaufwerke, integrierte oder externe USB-Festplatten oder Onlinespeicher überträgt.
<p></p>
<a href="https://duplicati.com/" target="_blank">Duplicati</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: "2.1"
services: services:
duplicati: duplicati:
image: ghcr.io/linuxserver/duplicati image: ghcr.io/linuxserver/duplicati
@@ -6,11 +7,10 @@ services:
- PUID=0 - PUID=0
- PGID=0 - PGID=0
- TZ=Europe/Berlin - TZ=Europe/Berlin
- DUPLICATI__WEBSERVICE_PASSWORD=${PASS} - CLI_ARGS= #optional
volumes: volumes:
- ./config:/config - ./config:/config
- backup:/backups - backup:/backups
- Immich:/mnt/immich
- /opt:/source - /opt:/source
- ./shared:/shared - ./shared:/shared
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
@@ -24,14 +24,6 @@ volumes:
name: backup name: backup
driver: local driver: local
driver_opts: driver_opts:
type: nfs4 type: nfs
o: addr=${NFS_SERVER},rw o: addr=${NFS_SERVER},rw
device: ":${PFAD}" device: ":${PFAD}"
Immich:
name: Immich
driver: local
driver_opts:
type: nfs4
o: addr=${NFS_SERVER},rw
device: ":${PFAD2}"

8
filerun/.env.sample Normal file
View File

@@ -0,0 +1,8 @@
#MySQL Datenbank
MYSQL_USER=changeMe
MYSQL_PASSWORD=changeMe
MYSQL_ROOT_PASSWORD=changeMe
NFS_SERVER=192.168.0.xxx # oder hostname oder Domain
PFAD_1=/Pfad/zum/Ordner/www
PFAD_2=/Pfad/zum/Ordner/energie

View File

@@ -0,0 +1,45 @@
version: '2'
services:
db:
image: mariadb:10.1
container_name: filerun_db
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: filerun
restart: unless-stopped
volumes:
- ./db:/var/lib/mysql
filerun:
image: filerun/filerun
container_name: filerun_app
environment:
FR_DB_HOST: db
FR_DB_PORT: 3306
FR_DB_NAME: filerun
FR_DB_USER: ${MYSQL_USER}
FR_DB_PASS: ${MYSQL_PASSWORD}
depends_on:
- db
links:
- db:db
ports:
- "8013:80"
restart: unless-stopped
volumes:
- ./html:/var/www/html
- ./user-files:/user-files
- Dokumente:/user-files/Dokumente
volumes:
Dokumente:
name: Dokumente
driver: local
driver_opts:
type: nfs
o: addr=${NFS_SERVER},rw
device: ":${PFAD_Dokumente}"

View File

@@ -0,0 +1,37 @@
version: "3"
services:
mariadb:
image: mariadb:10.1.11
container_name: fredbet_db
volumes:
- /opt/fredbet/mariadb:/var/lib/mysql
ports:
- "3307:3306"
environment:
- MYSQL_DATABASE=fredbetdb
- MYSQL_ROOT_PASSWORD=secred
- MYSQL_USER=fred
- MYSQL_PASSWORD=fred
restart: unless-stopped
fredbet:
image: fred4jupiter/fredbet
container_name: fredbet
links:
- mariadb:mariadb
depends_on:
- mariadb
ports:
- "8083:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:mariadb://mariadb:3306/fredbetdb
- SPRING_DATASOURCE_USERNAME=fred
- SPRING_DATASOURCE_PASSWORD=fred
- SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
- FREDBET_IMAGE_LOCATION_FILE=FILE_SYSTEM
- FREDBET_IMAGE_FILE_SYSTEM_BASE_FOLDER=/home/fred/fredbet_images
volumes:
- "./fredbet_images:/home/fred/fredbet_images"
- "./tmp:/tmp"
restart: unless-stopped

View File

@@ -1,10 +0,0 @@
<h2>Gitea</H2>
<p></p>
<p></p>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Gitea_Logo.svg/2560px-Gitea_Logo.svg.png" alt="Gitea" width="200" />
<p></p>
<p></p>
<b>Gitea</b> Gitea ist eine freie, in Go entwickelte Softwarelösung, die eine gehostete Softwareentwicklungsplattform bereitstellt. Diese unterstützt neben der Versionsverwaltung über Git auch kollaborative Werkzeuge, wie Bugtracker, Wiki und Code-Review. Die Benutzeroberfläche orientiert sich an GitHub.
<p></p>
<a href="https://about.gitea.com/" target="_blank">Gitea</a>
<p></p>

View File

@@ -1,3 +1,5 @@
version: "3"
services: services:
gitea: gitea:
image: gitea/gitea image: gitea/gitea
@@ -5,7 +7,7 @@ services:
environment: environment:
- USER_UID=1000 - USER_UID=1000
- USER_GID=1000 - USER_GID=1000
restart: unless-stopped restart: always
volumes: volumes:
- ./gitea:/data - ./gitea:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro

View File

@@ -1,12 +0,0 @@
<h2>Gotify</H2>
<p></p>
<p></p>
<img src="https://avatars.githubusercontent.com/u/36410427?s=200&v=4" alt="Gotify" width="200" />
<p></p>
<p></p>
<b>Gotify</b> ist ein simpler Server zum Senden (via REST-API) und Empfangen (via Websocket) von Nachrichten.
<p></p>
<a href="https://gotify.net/" target="_blank">Gotify</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: "2"
services: services:
gotify: gotify:
container_name: gotify container_name: gotify

View File

@@ -1,10 +0,0 @@
<h2>Grafana</H2>
<p></p>
<p></p>
<img src="https://cdn.icon-icons.com/icons2/2699/PNG/512/grafana_logo_icon_171049.png" alt="Grafana" width="200" />
<p></p>
<p></p>
<b>Grafana</b> ist eine plattformübergreifende Open-Source-Anwendung zur grafischen Darstellung von Daten aus verschiedenen Datenquellen wie z. B. InfluxDB, MySQL, PostgreSQL, Prometheus und Graphite. Die erfassten Rohdaten lassen sich anschließend in verschiedenen Anzeigeformen ausgeben.
<p></p>
<a href="https://grafana.com/" target="_blank">Grafana</a>
<p></p>

View File

@@ -1,36 +1,21 @@
version: '3'
services: services:
influxdb: influxdb:
#image: influxdb
image: influxdb:1.6.4 image: influxdb:1.6.4
container_name: influxdb container_name: influxdb
ports: ports:
- 8096:8086 - 8096:8086
#- 8098:8088
volumes: volumes:
- ./influxdb:/var/lib/influxdb - ./influxdb:/var/lib/influxdb
environment: environment:
- INFLUXDB_DB=METRICS - INFLUXDB_DB=METRICS
- INFLUXDB_USERNAME=${INFLUXDB_USERNAME} - INFLUXDB_USERNAME=${INFLUXDB_USERNAME}
- INFLUXDB_PASSWORD=${INFLUXDB_PASSWORD} - INFLUXDB_PASSWORD=${INFLUXDB_PASSWORD}
- INFLUXDB_REPORTING_DISABLED=true networks:
#Determines which level of logs will be emitted. The available levels are error, warn, info, and debug. - external_network
- INFLUXDB_LOGGING_LEVEL=warn
restart: unless-stopped restart: unless-stopped
chronograf:
image: chronograf:1.6
container_name: chronograf
volumes:
- ./chronograf_data:/var/lib/chronograf
ports:
- 8088:8888
depends_on:
- influxdb
environment:
- INFLUXDB_URL=http://influxdb:8086
- INFLUXDB_USERNAME=${INFLUXDB_USERNAME}
- INFLUXDB_PASSWORD=${INFLUXDB_PASSWORD}
restart: always
grafana: grafana:
image: grafana/grafana:latest image: grafana/grafana:latest
container_name: grafana container_name: grafana
@@ -44,4 +29,10 @@ services:
environment: environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_USERNAME} - GF_SECURITY_ADMIN_USER=${GRAFANA_USERNAME}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD} - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
networks:
- external_network
restart: unless-stopped restart: unless-stopped
networks:
external_network:
external: true

View File

@@ -1,10 +0,0 @@
<h2>Grocy</H2>
<p></p>
<p></p>
<img src="https://upload.wikimedia.org/wikipedia/commons/4/45/Grocy_logo.svg" alt="Grocy" width="200" />
<p></p>
<p></p>
<b>Grocy</b> ist eine selbst gehostete Webanwendung zur Lebensmittel- und Haushaltsverwaltung.
<p></p>
<a href="https://grocy.info/de/" target="_blank">Grocy</a>
<p></p>

View File

@@ -1,3 +1,5 @@
---
version: "2.1"
services: services:
grocy: grocy:
image: lscr.io/linuxserver/grocy image: lscr.io/linuxserver/grocy

View File

@@ -1,10 +0,0 @@
<h2>Heimdall</H2>
<p></p>
<p></p>
<img src="https://spin.atomicobject.com/wp-content/uploads/heimdall-icon-large2.png" alt="Heimdall" width="200" />
<p></p>
<p></p>
<b>Heimdall</b> ist ein Dashboard für alle Ihre Webanwendungen. Es muss jedoch nicht auf Anwendungen beschränkt sein, Sie können Links zu allem hinzufügen, was Sie möchten.
<p></p>
<a href="https://heimdall.site/" target="_blank">Heimdall</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: "3"
services: services:
heimdall: heimdall:
image: ghcr.io/linuxserver/heimdall image: ghcr.io/linuxserver/heimdall

View File

@@ -1,11 +0,0 @@
<h2>Home Assistant</H2>
<p></p>
<p></p>
<img src="https://www.it-und-hausautomation-blog.de/wp-content/uploads/2022/12/1200px-Home_Assistant_Logo.svg.png" alt="Home Assistant" width="200" />
<p></p>
<p></p>
<p><b>Home Assistant</b> Home Assistant ist eine kostenlose und quelloffene Software zur Hausautomation, die als zentrales Steuerungssystem in einem Smart Home oder Smart House konzipiert ist. Geschrieben in Python liegt ihr Hauptaugenmerk auf lokaler Steuerung und Privatsphäre.
</p>
<p></p>
<a href="https://www.home-assistant.io/" target="_blank">Home Assistant</a>
<p></p>

View File

@@ -1,16 +0,0 @@
- id: '1734541261890'
alias: Batteriewarnung X5
description: ''
triggers:
- trigger: numeric_state
entity_id:
- sensor.max_watch_battery_max
below: 29.9
conditions: []
actions:
- action: notify.mobile_app_s20_lars
metadata: {}
data:
message: Batterie X5 unter 30%
title: Warnung
mode: single

View File

@@ -1,37 +0,0 @@
# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:
# Text to speech
tts:
- platform: google_translate
group: !include groups.yaml
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
# Localhost als erlaubter zugang zulassen
homeassistant:
external_url: "https://ha.borgal.de"
internal_url: "https://docker:8123"
customize:
sensor.bitshake_smartmeterreader_aktuelle_wirkleistung:
unit_of_measurement: "W"
device_class: energy
sensor.bitshake_smartmeterreader_total_in:
unit_of_measurement: "kWh"
device_class: energy
sensor.bitshake_smartmeterreader_total_out:
unit_of_measurement: "kWh"
device_class: energy
http:
use_x_forwarded_for: true
trusted_proxies:
#- 0.0.0.0
- 192.168.0.111 # IP Docker
- 10.1.0.0/16 # IP Range Docker Container

View File

@@ -1,3 +1,4 @@
version: '3'
services: services:
homeassistant: homeassistant:
container_name: homeassistant container_name: homeassistant
@@ -7,15 +8,12 @@ services:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /dev/serial/by-id:/dev/serial/by-id - /dev/serial/by-id:/dev/serial/by-id
devices: devices:
# - /dev/ttyACM0:/dev/ttyACM0 - /dev/ttyACM0:/dev/ttyACM0
- /dev/ttyUSB0:/dev/ttyUSB0
ports: ports:
- 8123:8123 - "192.168.0.111:8123:8123"
privileged: true privileged: true
environment: environment:
- TZ=Europe/Berlin - TZ=Europe/Berlin
labels: # labels:
- com.centurylinklabs.watchtower.enable=true # - "com.centurylinklabs.watchtower.monitor-only=true"
restart: unless-stopped restart: unless-stopped
network_mode: host

View File

@@ -1,12 +0,0 @@
<h2>HP-Scan-to</H2>
<p></p>
<p></p>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ad/HP_logo_2012.svg/2048px-HP_logo_2012.svg.png" alt="HP-Scan-to" width="200" />
<p></p>
<p></p>
<p><b>HP-Scan-to</b> ist eine Node.js-Anwendung, die die Funktionalität des „Scan to Computer“ von HP nachbildet. Zu diesem Zweck wurde die Interaktion der ursprünglichen HP Windows-Anwendung mit dem Gerät zurückentwickelt. Sein Hauptzweck besteht darin, Benutzern das Scannen von Dokumenten direkt von einem HP-Gerät und die nahtlose Übertragung auf einen Computer zu ermöglichen. Im Gegensatz zum Originalprogramm ist dieses Programm so konzipiert, dass es mit Linux (einschließlich Docker) kompatibel ist.
</p>
<p></p>
<a href="https://github.com/manuc66/node-hp-scan-to" target="_blank">HP-Scan-to</a>
<p></p>

View File

@@ -1,3 +1,5 @@
version: "3"
services: services:
hp-scan: hp-scan:
image: manuc66/node-hp-scan-to:latest image: manuc66/node-hp-scan-to:latest
@@ -5,9 +7,8 @@ services:
hostname: TrueNAS hostname: TrueNAS
environment: environment:
- IP=192.168.0.11 - IP=192.168.0.11
- Label=TrueNas-Scan
- PATTERN="scan"_dd.mm.yyyy_hh:MM:ss - PATTERN="scan"_dd.mm.yyyy_hh:MM:ss
- PGID=1001 - PGID=1000
- PUID=1000 - PUID=1000
- TZ=Europe/Berlin - TZ=Europe/Berlin
labels: labels:
@@ -21,6 +22,6 @@ volumes:
name: scan name: scan
driver: local driver: local
driver_opts: driver_opts:
type: nfs4 type: nfs
o: addr=${NFS_SERVER},rw o: addr=${NFS_SERVER},rw
device: ":${PFAD}" device: ":${PFAD}"

View File

@@ -1,11 +0,0 @@
<h2>IMAP-Filter</H2>
<p></p>
<p></p>
<img src="https://gnulinux.ch/bl-content/uploads/pages/b0acef4f64d6f73352cdcc0b9f1c946a/brett-jordan-LPZy4da9aRo-unsplash.jpg" alt="IMAP-Filter" width="200" />
<p></p>
<p></p>
<p><b>IMAP-Filter</b> ist ein E-Mail-Filterdienstprogramm. Es stellt eine Verbindung zu Remote-Mailservern her und sendet mithilfe des Internet Message Access Protocol (IMAP) Suchanfragen an den Server. Es verarbeitet Postfächer basierend auf den Ergebnissen. Es kann verwendet werden zum Löschen, kopieren, verschieben, kennzeichnen usw. von Nachrichten, die sich gleichzeitig in Postfächern oder auf verschiedene Mailservern befinden.
</p>
<p></p>
<a href="https://github.com/lefcha/imapfilter" target="_blank">IMAP-Filter</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: "2"
services: services:
imapfilter: imapfilter:
build: . build: .

View File

@@ -1,26 +0,0 @@
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
# The location where your uploaded files are stored
UPLOAD_LOCATION=Immich
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
IMMICH_VERSION=release
# Connection secrets for postgres and typesense. You should change these to random passwords
TYPESENSE_API_KEY=some-random-text
DB_PASSWORD=ChangeMe
IMMICH_HOST=0.0.0.0
PUBLIC_URL=https://immich.domain.de
# The values below this line do not need to be changed
###################################################################################
DB_HOSTNAME=immich_postgres
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
REDIS_HOSTNAME=immich_redis
# NFS Share
###################################################################################
NFS_SERVER=192.168.0.xxx
PFAD=/pfad/zum/nfs/share

View File

@@ -1,11 +0,0 @@
<h2>Immich</H2>
<p></p>
<p></p>
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTEZ3wPH-RD0U8S7h9Zovh4jMEUINrBBIeD-g&s" alt="Immich" width="200" />
<p></p>
<p></p>
<p><b>Immich</b> ist eine High Performance self-hosted Foto und Video Management Lösung.
</p>
<p></p>
<a href="https://immich.app/" target="_blank">Immich</a>
<p></p>

View File

@@ -1,63 +0,0 @@
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- 2283:2283
depends_on:
- redis
- database
labels:
- "com.centurylinklabs.watchtower.monitor-only=true"
restart: always
immich-machine-learning:
container_name: immich_machine_learning
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
volumes:
- ./model-cache:/cache
env_file:
- .env
labels:
- "com.centurylinklabs.watchtower.monitor-only=true"
restart: always
redis:
container_name: immich_redis
image: redis:6.2-alpine
volumes:
- ./redis:/data
labels:
- "com.centurylinklabs.watchtower.monitor-only=true"
restart: always
database:
container_name: immich_postgres
image: tensorchord/pgvecto-rs:pg14-v0.2.0
env_file:
- .env
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ./DB-data:/var/lib/postgresql/data
labels:
- "com.centurylinklabs.watchtower.monitor-only=true"
command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
volumes:
Immich:
name: Immich
driver: local
driver_opts:
type: nfs4
o: addr=${NFS_SERVER},rw
device: ":${PFAD}"

View File

@@ -1,10 +0,0 @@
# MyJDownloader
###################################################################################
MYJ_MAIL=mail@domain.de
MYJ_PASSWORD=ChangeMe
# NFS Share
###################################################################################
NFS_SERVER=192.168.0.xxx
PFAD=/Pfad/zum/Ziel

View File

@@ -1,11 +0,0 @@
<h2>jDownloader</H2>
<p></p>
<p></p>
<img src="https://upload.wikimedia.org/wikipedia/commons/f/f7/Jdownloader.png" alt="jDownloader" width="200" />
<p></p>
<p></p>
<p><b>jDownloader</b> ist ein in Java geschriebener Download-Manager, der primär für das automatisierte Herunterladen bei Sharehostern entwickelt wurde. Zusätzlich ermöglicht das Programm das Herunterladen von Videos von Videoportalen wie z. B. YouTube.
</p>
<p></p>
<a href="https://adguard.com/de/adguard-home/overview.html/" target="_blank">jDownloader</a>
<p></p>

View File

@@ -1,28 +0,0 @@
services:
jdownloader-2:
image: jlesage/jdownloader-2
container_name: jdownloader2
ports:
- 5800:5800
- 3129:3129
environment:
- PUID=1000
- PGID=1001
- LANG=de_DE.UTF-8
- TZ=Europe/Berlin
- KEEP_APP_RUNNING=1
- MYJDOWNLOADER_EMAIL=${MAIL}
- MYJDOWNLOADER_PASSWORD=${PASSWORD}
- MYJDOWNLOADER_DEVICE_NAME=Docker
volumes:
- ./config:/config
- jdownloader:/output
volumes:
jdownloader:
name: jdownloader
driver: local
driver_opts:
type: nfs4
o: addr=${NFS_SERVER},rw
device: ":${PFAD}"

View File

@@ -1,5 +0,0 @@
NFS_SERVER=192.168.0.xxx
PFAD=/Pfad/zu/den/Videos
#
# Auf Rechte achten
#

View File

@@ -1,28 +0,0 @@
services:
jellyfin:
image: lscr.io/linuxserver/jellyfin:latest
container_name: jellyfin
environment:
- PUID=1000
- PGID=1001
- TZ=Europe/Berlin
#- JELLYFIN_PublishedServerUrl=http://192.168.0.5 #optional
volumes:
- ./config:/config
#- /path/to/tvseries:/data/tvshows
- video:/data/movies
ports:
- 8097:8096
#- 8920:8920 #optional
#- 7359:7359/udp #optional
#- 1900:1900/udp #optional
restart: unless-stopped
volumes:
video:
name: video
driver: local
driver_opts:
type: nfs4
o: addr=${NFS_SERVER},rw
device: ":${PFAD}"

View File

@@ -1,22 +0,0 @@
services:
kasm:
image: lscr.io/linuxserver/kasm:latest
container_name: kasm
privileged: true
security_opt:
- apparmor:rootlesskit #optional
environment:
- KASM_PORT=8443
- TZ=Europe/Berlin
- PGID=1000
- PUID=1001
volumes:
- ./data:/opt
- ./profiles:/profiles
- ./dev/input:/dev/input
- ./run/udev/data:/run/udev/data
ports:
- 3001:3000
- 8443:8443
restart: unless-stopped

View File

@@ -1,91 +0,0 @@
<h2>knxd</H2>
<p></p>
<p></p>
<img src="https://bookstack.borgal.de/uploads/images/gallery/2021-04/1200px-knx-logo.png" alt="knxd" width="200" />
<p></p>
<p></p>
<p><b>knxd</b> ist ein weit verbreitetes Protokoll für die Gebäudeautomation, das über eine dedizierte 9600-Baud-Leitung sowie IP-Multicast läuft. knxd ist ein fortschrittlicher Router/Gateway, der auf jedem Linux-Computer läuft. Es kann mit allen bekannten KNX-Schnittstellen kommunizieren.
</p>
<p></p>
<a href="https://github.com/henfri/docker" target="_blank">knxd v0.12.6</a>
<p></p>
<h3>HowTo</h3>
<hr>
<p></p>
<p>Config</p>
<pre>services:
knxd:
image: henfri/knxd:v0.12.6
container_name: knxd
network_mode: "host"
command: knxd -t 1022 -e 1.1.250 -E 1.1.230:8 -f9 -DTRS -c -B single --send-delay=120 -b ipt:192.168.0.200
restart: always</pre>
<p id="bkmrk--t-1022-und--f9-ist-" class="callout info">-t 1022 und -f9 ist f&uuml;r loggin, kann wenn es keine Probleme gibt auch aus geschaltet werden.</p>
<p id="bkmrk-backup" class="callout danger"><strong><span style="color: #ff0000;">Schnittstelle ben&ouml;tigt ca. 1 Minute bis sie l&auml;uft.</span></strong></p>
<p id="bkmrk-%C2%A0"></p>
<h3 id="bkmrk-backup-0">Backup</h3>
<hr id="bkmrk--0">
<p id="bkmrk-ab-version-14-muss-m" class="callout warning"><span style="color: #ff0000;">Ab Version 14 muss mit config.ini Datei gearbeitet werden, gab bei mir Probleme daher wieder Version 12.6</span></p>
<p id="bkmrk-%C2%A0-1"></p>
<p id="bkmrk-docker-hub"><a href="https://hub.docker.com/r/tekn0ir/knxd" target="_blank" rel="noopener">Docker-Hub</a></p>
<p id="bkmrk-docker-compose-beisp"><a href="https://github.com/julakali/smarthome-compose" target="_blank" rel="noopener">Docker-Compose Beispiel</a></p>
<p id="bkmrk-original-aus-alter-k"><span style="color: #003366;"><em>Original aus alter knxd Config</em></span></p>
<pre id="bkmrk-knxd_opts%3D%22-e-1.1.25"><code class="language-">KNXD_OPTS="-e 1.1.254 -E 1.1.200:8 -D -T -R -S -B single -b ipt:192.168.0.200"</code></pre>
<p id="bkmrk-docker-compose.yml-0"><span style="color: #003366;"><em>docker-compose.yml</em></span></p>
<pre id="bkmrk-version%3A-%273.4%27-servi"><code class="language-">version: '3.4'
services:
knxd:
image: renehezser/knxd
container_name: knxd
ports:
- 6720:6720
- 3671:3671
volumes:
- /opt/knxd/config.ini:/config.ini
restart: always
network_mode: host</code></pre>
<p id="bkmrk-config.ini"><span style="color: #003366;">config.ini</span></p>
<pre id="bkmrk-%5Bmain%5D-name-%3D-knxd-a"><code class="language-">[main]
name = knxd
addr = 1.1.254
client-addrs = 1.1.200:8
connections = server,B.tcp,C.ipt
cache = A.cache
systemd = systemd
[server]
debug = debug-server
discover = true
router = router
server = ets_router
tunnel = tunnel
[B.tcp]
server = knxd_tcp
systemd-ignore = true
filters = queue,D.filter
[C.ipt]&cedil;
driver = ipt
retry-delay = 30
filters = single,queue,D.filter
ip-address = 192.168.0.200
debug = debug-server
[D.filter]
delay = 20
filter = pace
[debug-server]
name = mcast:knxd
#error-level = 0x9
#trace-mask = 0xffc</code></pre>
<p id="bkmrk-%C2%A0-2"></p>
<p id="bkmrk-service-auf-services">Service auf Serviceserver disabled</p>
<pre id="bkmrk-systemctl-disable-kn"><code class="language-">systemctl disable knxd.socket
systemctl disable knxd.service</code></pre>
<p id="bkmrk-%C2%A0-3"></p>

View File

@@ -1,18 +1,13 @@
version: "2"
services: services:
knxd: knxd:
image: henfri/knxd:v0.12.6 image: henfri/knxd:v0.12.6
container_name: knxd container_name: knxd
network_mode: "host" network_mode: "host"
logging:
options:
max-file: '2'
max-size: 10m
restart: always restart: always
# #
# mit logging, wenn es zu Problemen kommt # mit logging, wenn es zu Problemen kommt - zum aktivieren muss der Container entfernt werden!!!
command: knxd -e 1.1.250 -E 1.1.210:10 -f6 -t1023 -DTRS -c -B single --send-delay=150 -b ipt:192.168.0.200 command: knxd -t 1022 -e 1.1.250 -E 1.1.210:49 -f9 -DTRS -c -B single --send-delay=120 -b ipt:192.168.0.200
# #
# ohne logging -i deaktiviert zum testen # ohne logging
# # command: knxd -e 1.1.250 -E 1.1.210:30 -DTRS -c -i --send-delay=120 -B single -b ipt:192.168.0.200
# command: knxd -e 1.1.250 -E 1.1.210:8 -DTRS -c -B single --send-delay=30 -b ipt:192.168.0.200

View File

@@ -1,11 +0,0 @@
<h2>Mealie</H2>
<p></p>
<p></p>
<img src="https://static-00.iconduck.com/assets.00/mealie-icon-512x489-939jw8dj.png" width="200" />
<p></p>
<p></p>
<p><b>Mealie</b> ist eine intuitive und einfach zu bedienende Rezeptverwaltungs-App. Es wurde entwickelt, um Ihnen das Leben zu erleichtern, indem es das beste Rezeptverwaltungserlebnis im Internet bietet und Ihnen eine benutzerfreundliche Oberfläche zur Verwaltung Ihrer wachsenden Rezeptsammlung bietet.
</p>
<p></p>
<a href="https://mealie.io/" target="_blank">Mealie</a>
<p></p>

View File

@@ -1,41 +0,0 @@
services:
mealie:
container_name: mealie
image: hkotel/mealie:latest
restart: unless-stopped
volumes:
- ./data:/app/data
ports:
- 9925:9000
environment:
ALLOW_SIGNUP: "false"
BASE_URL: "https://rezepte.borgal.de"
API_DOCS: "false"
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
# =====================================
# Postgres Config
POSTGRES_USER: mealie
POSTGRES_PASSWORD: mealie
POSTGRES_SERVER: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: mealie
# Webworker
# =====================================
# Web Concurrency
WEB_GUNICORN: "false"
WORKERS_PER_CORE: 0.5
MAX_WORKERS: 1
WEB_CONCURRENCY: 1
# Mail settings
# =====================================
# Email Configuration
# SMTP_HOST=
# SMTP_PORT=587
# SMTP_FROM_NAME=Mealie
# SMTP_AUTH_STRATEGY=TLS # Options: 'TLS', 'SSL', 'NONE'
# SMTP_FROM_EMAIL=
# SMTP_USER=
# SMTP_PASSWORD=

View File

@@ -1,11 +0,0 @@
<h2>Mumble</H2>
<p></p>
<p></p>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Icons_mumble.svg/1024px-Icons_mumble.svg.png" width="200" />
<p></p>
<p></p>
<p><b>Mumble</b> ist eine freie Sprachkonferenzsoftware, die sich wegen niedriger Latenzzeit und guter Audioqualität unter anderem für den Einsatz parallel zu Onlinespielen eignet.
</p>
<p></p>
<a href="https://www.mumble.info/" target="_blank">Mumble</a>
<p></p>

View File

@@ -1,3 +1,5 @@
version: '3'
services: services:
murmur: murmur:
image: vimagick/murmur image: vimagick/murmur

View File

@@ -1,11 +0,0 @@
<h2>Node-RED</H2>
<p></p>
<p></p>
<img src="https://blog.quindorian.org/wp-content/uploads/2019/05/node-red-logo.png" width="200" />
<p></p>
<p></p>
<p><b>Node-RED</b> ist ein von IBM entwickeltes grafisches Entwicklungswerkzeug. Die Software ermöglicht es, Anwendungsfälle im Bereich des Internets der Dinge mit einem einfachen Baukastenprinzip umzusetzen. Die einzelnen Funktionsbausteine werden durch Ziehen von Verbindungen verbunden.
</p>
<p></p>
<a href="https://nodered.org/" target="_blank">Node-RED</a>
<p></p>

View File

@@ -1,3 +1,5 @@
version: '2.1'
services: services:
nodered: nodered:
container_name: node-red container_name: node-red

View File

@@ -1,11 +0,0 @@
<h2>OnlyOffice</H2>
<p></p>
<p></p>
<img src="https://media.graphassets.com/X8UAyIhwQom80fua3x8T" width="200" />
<p></p>
<p></p>
<p><b>OnlyOffice</b> ist ein neues Office-Programm, das auf Geschwindigkeit und Cloud-Anbindung setzt. Gleichzeitig werden alle Formate von MS Office unterstützt (DOCX, XLSX, PPTX), auch Open-Document Formate können problemlos bearbeitet werden. Zwar kommt das Open-Source-Programm bislang mit weniger Features aus als die Konkurrenz, punktet dafür aber mit innovativen Neuerungen..
</p>
<p></p>
<a href="https://www.onlyoffice.com/de/" target="_blank">OnlyOffice</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: '3'
services: services:
onlyoffice-document-server: onlyoffice-document-server:
container_name: onlyoffice-document-server container_name: onlyoffice-document-server
@@ -13,7 +14,3 @@ services:
- ./document_log:/var/log/onlyoffice - ./document_log:/var/log/onlyoffice
- ./document_data:/var/www/onlyoffice/Data - ./document_data:/var/www/onlyoffice/Data
- ./document_lib:/var/lib/onlyoffice - ./document_lib:/var/lib/onlyoffice
- ./fonts:/usr/share/fonts/truetype/custom
- ./redis:/var/lib/redis
- ./db:/var/lib/postgresql
- ./rabbitmq:/var/lib/rabbitmq

View File

@@ -0,0 +1,26 @@
version: '2.2'
services:
openhab:
image: "openhab/openhab:3.0.1"
container_name: openhab
restart: always
network_mode: host
volumes:
- "/etc/localtime:/etc/localtime:ro"
- "/etc/timezone:/etc/timezone:ro"
- "./openhab_addons:/openhab/addons"
- "./openhab_conf:/openhab/conf"
- "./openhab_userdata:/openhab/userdata"
environment:
OPENHAB_HTTP_PORT: "8081"
OPENHAB_HTTPS_PORT: "8444"
EXTRA_JAVA_OPTS: "-Duser.timezone=Europe/Berlin"
volumes:
openhab_addons:
driver: local
openhab_conf:
driver: local
openhab_userdata:
driver: local

View File

@@ -1,11 +0,0 @@
<h2>OwnCloud</H2>
<p></p>
<p></p>
<img src="https://www.etes.de/files/etes/logo/ownCloud_Logo.png" alt="Owncloud" width="200" />
<p></p>
<p></p>
<p><b>OwnCloud</b> ist ein Open-Source-Software-Projekt, das eine Content Collaboration Plattform für den Einsatz in Unternehmen entwickelt, die das Speichern, Verteilen und gleichzeitige Bearbeiten von Daten auf eigenen Servern und Endgeräten organisiert..
</p>
<p></p>
<a href="https://owncloud.com/de/" target="_blank">OwnCloud</a>
<p></p>

View File

@@ -1,3 +1,5 @@
version: "3"
services: services:
owncloud: owncloud:
image: owncloud/server image: owncloud/server
@@ -21,17 +23,17 @@ services:
- OWNCLOUD_MYSQL_UTF8MB4=true - OWNCLOUD_MYSQL_UTF8MB4=true
- OWNCLOUD_REDIS_ENABLED=true - OWNCLOUD_REDIS_ENABLED=true
- OWNCLOUD_REDIS_HOST=redis - OWNCLOUD_REDIS_HOST=redis
# healthcheck: healthcheck:
# test: ["CMD", "/usr/bin/healthcheck"] test: ["CMD", "/usr/bin/healthcheck"]
# interval: 30s interval: 30s
# timeout: 10s timeout: 10s
# retries: 5 retries: 5
volumes: volumes:
- ./files:/mnt/data - ./files:/mnt/data
mariadb: mariadb:
image: mariadb:10.11 image: mariadb:10.6 # minimum required ownCloud version is 10.9
container_name: owncloud_mariadb container_name: owncloud_mariadb
restart: always restart: always
environment: environment:

View File

@@ -1,160 +0,0 @@
<h2>Paperless ngx</H2>
<p></p>
<p></p>
<img src="https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/paperless-ngx-banner.png" alt="Paperless" width="200" />
<p></p>
<p></p>
<p><b>Paperless ngx</b> ist ein von der Community unterstütztes Open-Source-Dokumentenverwaltungssystem, das Ihre physischen Dokumente in ein durchsuchbares Online-Archiv umwandelt, sodass Sie weniger Papier benötigen.
</p>
<p></p>
<a href="https://docs.paperless-ngx.com/" target="_blank">Paperless ngx</a>
<p></p>
<h2>HowTo</h2>
<hr>
<p></p>
<h4>/etc/fstab Mount zu Scanner Verzeichniss</h4>
<p></p>
<pre>192.168.0.102:/mnt/pool1/scanner /opt/paperless/consume nfs auto 0 0</pre>
<p></p>
<h5>gegebenfalls noch die NFS Tools installieren</h5>
<pre>apt install nfs-common</pre>
<p></p>
# Update Paperless-ngx mit DB-upgrade !
### Schritt 1: Datenbank sichern und vorbereiten
1. **Stoppen Sie den `webserver`-Dienst**, um zu verhindern, dass die Anwendung während des Backups Daten in die Datenbank schreibt. Der `db`-Dienst muss dabei **laufen**.
```
docker compose stop webserver
```
2. **Überprüfen Sie, ob der `db`-Dienst läuft**.
```
docker compose ps
```
Der Status von `paperless_db` sollte `Up` sein. Wenn er nicht läuft, müssen Sie die Logs mit `docker compose logs db` überprüfen, um das Problem zu beheben, bevor Sie fortfahren.
3. **Führen Sie den Backup-Befehl aus**. Dieser Befehl startet `pg_dump` innerhalb des laufenden `db`-Containers und speichert die Ausgabe in eine Datei auf Ihrem Host.
```
docker compose exec db pg_dump -U paperless -Fc paperless > paperless_db_backup.dump
```
Eine komprimierte Backup-Datei namens `paperless_db_backup.dump` wird im selben Verzeichnis wie Ihre `docker-compose.yml` erstellt.
----------
### Schritt 2: `docker-compose.yml` aktualisieren
1. **Stoppen Sie alle Dienste**, um eine saubere Ausgangsbasis zu schaffen.
```
docker compose down
```
2. **Bearbeiten Sie Ihre `docker-compose.yml`-Datei.**
- Ändern Sie das `image` für den `db`-Dienst von `postgres:13` auf `postgres:17`.
- **Wichtig:** Ändern Sie den `volumes`-Pfad für die neuen Datenbankdaten, um zu verhindern, dass die alten Daten überschrieben werden. PostgreSQL-Versionen sind nicht direkt kompatibel.
YAML
```
# ...
services:
# ...
db:
image: docker.io/library/postgres:17 # Version geändert
container_name: paperless_db
restart: unless-stopped
volumes:
- ./db_v17_data:/var/lib/postgresql/data # WICHTIG: Pfad geändert
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
labels:
- "com.centurylinklabs.watchtower.monitor-only=true"
# ...
```
3. **Starten Sie nur den neuen Datenbank-Container**.
```
docker compose up -d db
```
Dadurch wird ein neuer, leerer PostgreSQL 17-Container erstellt.
4. **Importieren über einen temporären Ordner**.
```
docker compose cp paperless_db_backup.dump db:/tmp/paperless_db_backup.dump
```
Dieser Befehl kopiert Ihre lokale Backup-Datei in das `/tmp`-Verzeichnis des laufenden `db`-Containers.
2. **Stellen Sie das Backup im Container wieder her:**
```
docker compose exec db sh -c "pg_restore -U paperless -d paperless /tmp/paperless_db_backup.dump"
```
Hierbei übergeben Sie den Pfad der Datei direkt an den `pg_restore`-Befehl, anstatt die Eingabe umzuleiten.
### Letzter Schritt: Dienste starten und bereinigen
1. **Starten Sie die Paperless-ngx-Dienste:** Führen Sie den folgenden Befehl aus, um alle Dienste, einschließlich des `webserver`s, zu starten.
Bash
```
docker compose up -d
```
2. **Überprüfen Sie die Funktionalität:** Öffnen Sie die Paperless-ngx-Weboberfläche in Ihrem Browser. Melden Sie sich an und überprüfen Sie, ob Ihre Dokumente korrekt angezeigt werden und die Anwendung wie erwartet funktioniert.
3. **Bereinigen Sie die temporären Dateien:** Wenn alles einwandfrei läuft, können Sie die erstellten Sicherungsdateien löschen.
Bash
```
rm paperless_db_backup.dump
```
Sie können auch das alte Datenbank-Volume löschen, um Speicherplatz freizugeben.
4. **Löschen Sie die alte Datenbank (optional):** Falls Sie das alte Datenbank-Volume nicht mehr benötigen, können Sie es ebenfalls entfernen. **Seien Sie hier vorsichtig, um keine wichtigen Daten zu verlieren.**
Bash
```
docker volume ls # find the old volume name
docker volume rm <old_volume_name>
```
Herzlichen Glückwunsch, Ihre PostgreSQL-Datenbank wurde erfolgreich von Version 13 auf 17 migriert!

View File

@@ -1,8 +1,8 @@
# The UID and GID of the user used to run paperless in the container. Set this # The UID and GID of the user used to run paperless in the container. Set this
# to your UID and GID on the host so that you have write access to the # to your UID and GID on the host so that you have write access to the
# consumption directory. # consumption directory.
USERMAP_UID=1000 #USERMAP_UID=1000
USERMAP_GID=1001 #USERMAP_GID=1001
# Additional languages to install for text recognition, separated by a # Additional languages to install for text recognition, separated by a
# whitespace. Note that this is # whitespace. Note that this is
@@ -12,7 +12,7 @@ USERMAP_GID=1001
# default. # default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster # See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
# for available languages. # for available languages.
# PAPERLESS_OCR_LANGUAGES=tur ces #PAPERLESS_OCR_LANGUAGES=tur ces
############################################################################### ###############################################################################
# Paperless-specific settings # # Paperless-specific settings #
@@ -24,7 +24,7 @@ USERMAP_GID=1001
# Adjust this key if you plan to make paperless available publicly. It should # Adjust this key if you plan to make paperless available publicly. It should
# be a very long sequence of random characters. You don't need to remember it. # be a very long sequence of random characters. You don't need to remember it.
# PAPERLESS_SECRET_KEY=change-me #PAPERLESS_SECRET_KEY=change-me
# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC. # Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC.
PAPERLESS_TIME_ZONE=Europe/Berlin PAPERLESS_TIME_ZONE=Europe/Berlin
@@ -32,17 +32,7 @@ PAPERLESS_TIME_ZONE=Europe/Berlin
# The default language to use for OCR. Set this to the language most of your # The default language to use for OCR. Set this to the language most of your
# documents are written in. # documents are written in.
PAPERLESS_OCR_LANGUAGE=deu PAPERLESS_OCR_LANGUAGE=deu
PAPERLESS_OCR_LANGUAGES=deu
# Eigene Einstellungen
#
# PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=True
PAPERLESS_FILENAME_FORMAT={document_type}/{correspondent}/{created_year}/{title} PAPERLESS_FILENAME_FORMAT={document_type}/{correspondent}/{created_year}/{title}
PAPERLESS_CONSUMER_POLLING=10 PAPERLESS_CONSUMER_POLLING=30
PAPERLESS_URL=https://dms.borgal.de PAPERLESS_URL=https://dms.borgal.de
PAPERLESS_PRE_CONSUME_SCRIPT=/usr/src/paperless/scripts/pre-consume.sh
# QR Code Scanner
PAPERLESS_CONSUMER_ENABLE_BARCODES=true
PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=true
PAPERLESS_CONSUMER_BARCODE_SCANNER=ZXING

View File

@@ -26,6 +26,7 @@
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
version: "3.4"
services: services:
broker: broker:
image: docker.io/library/redis:7 image: docker.io/library/redis:7
@@ -44,56 +45,36 @@ services:
POSTGRES_DB: paperless POSTGRES_DB: paperless
POSTGRES_USER: paperless POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless POSTGRES_PASSWORD: paperless
labels:
- "com.centurylinklabs.watchtower.monitor-only=true"
webserver: webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest image: ghcr.io/paperless-ngx/paperless-ngx
container_name: paperless_app container_name: paperless_app
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- db - db
- broker - broker
- gotenberg
- tika
ports: ports:
- 8009:8000/tcp - 8009:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- ./data:/usr/src/paperless/data - ./data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export - ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume - ./consume:/usr/src/paperless/consume
- ./scripts:/usr/src/paperless/scripts
- media:/usr/src/paperless/media
env_file: docker-compose.env env_file: docker-compose.env
environment: environment:
PAPERLESS_REDIS: redis://broker:6379 PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db PAPERLESS_DBHOST: db
PAPERLESS_BIND_ADDR: 0.0.0.0
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.7
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
- "--api-timeout=60"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes: volumes:
media: media:
name: media name: media
driver: local driver: local
driver_opts: driver_opts:
type: nfs4 type: nfs
o: addr=${NFS_SERVER},rw o: addr=${NFS_SERVER},rw
device: ":${PFAD}" device: ":${PFAD}"

View File

@@ -1,5 +0,0 @@
#!/bin/sh
set -x
# Remove blank pages
/usr/src/paperless/scripts/remove-blank-pages.sh

View File

@@ -1,49 +0,0 @@
#!/bin/bash
#set -x -e -o pipefail
set -e -o pipefail
export LC_ALL=C
#IN="$1"
IN="$DOCUMENT_WORKING_PATH"
# Check for PDF format
TYPE=$(file -b "$IN")
if [ "${TYPE%%,*}" != "PDF document" ]; then
>&2 echo "Skipping $IN - non PDF [$TYPE]."
exit 0
fi
# PDF file - proceed
#PAGES=$(pdfinfo "$IN" | grep ^Pages: | tr -dc '0-9')
PAGES=$(pdfinfo "$IN" | awk '/Pages:/ {print $2}')
>&2 echo Total pages $PAGES
# Threshold for HP scanners
# THRESHOLD=1
# Threshold for Lexmar MC2425
THRESHOLD=0.8
non_blank() {
for i in $(seq 1 $PAGES) ; do
PERCENT=$(gs -o - -dFirstPage=${i} -dLastPage=${i} -sDEVICE=ink_cov "${IN}" | grep CMYK | nawk 'BEGIN { sum=0; } {sum += $1 + $2 + $3 + $4;} END { printf "%.5f\n", sum } ')
>&2 echo -n "Color-sum in page $i is $PERCENT: "
if awk "BEGIN { exit !($PERCENT > $THRESHOLD) }"; then
echo $i
>&2 echo "Page added to document"
else
>&2 echo "Page removed from document"
fi
done
}
NON_BLANK=$(non_blank)
if [ -n "$NON_BLANK" ]; then
NON_BLANK=$(echo $NON_BLANK | tr ' ' ",")
qpdf "$IN" --warning-exit-0 --replace-input --pages . $NON_BLANK --
fi

4
photoview/.env.sample Normal file
View File

@@ -0,0 +1,4 @@
DB_PASS=ChangeMe
NFS_SERVER=192.168.0.x
PFAD=/mnt/Pfad/zuden/Bildern

View File

@@ -0,0 +1,62 @@
version: "3"
services:
db:
image: mariadb:10.5
container_name: photoview-db
restart: unless-stopped
environment:
- TZ=Europe/Berlin
- MYSQL_DATABASE=photoview
- MYSQL_USER=photoview
- MYSQL_PASSWORD=${DB_PASS}
- MYSQL_RANDOM_ROOT_PASSWORD=1
volumes:
- ./db_data:/var/lib/mysql
photoview:
image: viktorstrate/photoview:2.3.12
container_name: photoview-app
restart: unless-stopped
ports:
- "8012:80"
depends_on:
- db
environment:
- TZ=Europe/Berlin
- PHOTOVIEW_DATABASE_DRIVER=mysql
- PHOTOVIEW_MYSQL_URL=photoview:photosecret@tcp(db)/photoview
- PHOTOVIEW_LISTEN_IP=photoview
- PHOTOVIEW_LISTEN_PORT=80
- PHOTOVIEW_MEDIA_CACHE=/app/cache
- PHOTOVIEW_DISABLE_VIDEO_ENCODING=1
- PHOTOVIEW_DISABLE_RAW_PROCESSING=1
# Optional: If you are using Samba/CIFS-Share and experience problems with "directory not found"
# Enable the following Godebug
# - GODEBUG=asyncpreemptoff=1
# Optional: To enable map related features, you need to create a mapbox token.
# A token can be generated for free here https://account.mapbox.com/access-tokens/
# It's a good idea to limit the scope of the token to your own domain, to prevent others from using it.
- MAPBOX_TOKEN=pk.eyJ1IjoiYm9yZ2FsIiwiYSI6ImNsZnYxdGZiNTAyeXozbG83MHk5ZHVwZGUifQ.7HmlrT_LEAIyN1J_F0rzxA
volumes:
- ./api_cache:/app/cache
# Change This: to the directory where your photos are located on your server.
# If the photos are located at `/home/user/photos`, then change this value
# to the following: `/home/user/photos:/photos:ro`.
# You can mount multiple paths, if your photos are spread across multiple directories.
- Photos:/photos:ro
volumes:
Photos:
name: Photos
driver: local
driver_opts:
type: nfs
o: addr=${NFS_SERVER},rw
device: ":${PFAD}"

View File

@@ -1,11 +1,2 @@
<h2>Portainer</H2> <p id="bkmrk-"><a href="https://bookstack.borgal.de/uploads/images/gallery/2021-04/download.png" target="_blank" rel="noopener"><img src="https://bookstack.borgal.de/uploads/images/gallery/2021-04/scaled-1680-/download.png" alt="Download.png" /></a></p>
<p></p> <p id="bkmrk-docker-hub"><a href="https://hub.docker.com/r/portainer/portainer" target="_blank" rel="noopener">Docker-Hub</a></p>
<p></p>
<img src="https://bookstack.borgal.de/uploads/images/gallery/2021-04/download.png" alt="Portainer" width="200" />
<p></p>
<p></p>
<p><b>Portainer</b> ist Ihre Container-Management-Software zur Bereitstellung, Fehlerbehebung und Sicherung von Anwendungen in Cloud-, Rechenzentrums- und industriellen IoT-Anwendungsfällen.
</p>
<p></p>
<a href="https://www.portainer.io/" target="_blank">Portainer</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: "2"
services: services:
portainer: portainer:
container_name: portainer container_name: portainer

View File

@@ -13,16 +13,13 @@ wireshark | 3010
vdr | 3020 vdr | 3020
gitea | 3030 gitea | 3030
grafana | 3090 grafana | 3090
jdownloader | 3129
fredbed_db | 3307 fredbed_db | 3307
webserver_db | 3309 webserver_db | 3309
unifi controller | 3478 unifi controller | 3478
jdownloader | 5800
vdr | 6419 vdr | 6419
bookstack | 6875 bookstack | 6875
portainer | 8000 portainer | 8000
gotify | 8001 gotify | 8001
dockge | 8002
wekan | 8004 wekan | 8004
vdr | 8008 vdr | 8008
paperless | 8009 paperless | 8009
@@ -49,8 +46,6 @@ duplicati | 8200
openhab | 8444 openhab | 8444
youTubeDL | 8998 youTubeDL | 8998
portainer | 9000 portainer | 9000
mealie | 9925
dozzle | 9999
unifi controller | 10001 unifi controller | 10001
vdr-vnsi | 34890 vdr-vnsi | 34890
wireguard | 51820 wireguard | 51820

View File

@@ -1,11 +0,0 @@
<h2>Nginx Proxy Manager</H2>
<p></p>
<p></p>
<img src="https://mialikescoffee.com/images/nginx_logo.png" alt="Nginx Proxy Manager" width="200" />
<p></p>
<p></p>
<p><b>Nginx Proxy Manager</b> ist ein kostenloser, Open-Source-Proxy-Manager. Er bietet eine einfache und schnelle Oberfläche zum Konfigurieren und Verwalten von Proxy Hosts, einschließlich integriertem Letsencrypt. Dadurch muss man in den meisten Fällen weder die config Dateien manuell anfassen, noch sich um das SSL Zertifikat kümmern.
</p>
<p></p>
<a href="https://nginxproxymanager.com/" target="_blank">Nginx Proxy Manager</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: '3.8'
services: services:
app: app:
image: 'jc21/nginx-proxy-manager:latest' image: 'jc21/nginx-proxy-manager:latest'

View File

@@ -1,51 +0,0 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
rsync curl unzip python3 python3-pip \
inkscape poppler-utils cron \
php-cli \
&& rm -rf /var/lib/apt/lists/*
# rmc + Farb-Patch
RUN pip3 install "rmc==0.3.0" "rmscene==0.6.1" "pypdf" --break-system-packages --ignore-installed packaging \
&& python3 - <<'EOF'
import pathlib
import rmc.exporters.writing_tools as m
f = pathlib.Path(m.__file__)
src = f.read_text()
src = src.replace(
"self.base_color = RM_PALETTE[base_color_id]",
"self.base_color = RM_PALETTE.get(base_color_id, (255, 235, 0) if base_color_id == 9 else (128, 128, 128))"
)
old = ' segment_color = [min(int(abs(intensity - 1) * 255), 60)] * 3\n return "rgb" + str(tuple(segment_color))'
new = ' r = min(int(self.base_color[0] * intensity), 255)\n g = min(int(self.base_color[1] * intensity), 255)\n b = min(int(self.base_color[2] * intensity), 255)\n return "rgb" + str((r, g, b))'
src = src.replace(old, new)
old_b = ' segment_color = [int(rev_intensity * (255 - self.base_color[0])),\n int(rev_intensity * (255 - self.base_color[1])),\n int(rev_intensity * (255 - self.base_color[2]))]\n return "rgb" + str(tuple(segment_color))'
new_b = ' segment_color = [int(255 - intensity * (255 - c)) for c in self.base_color]\n return "rgb" + str(tuple(segment_color))'
src = src.replace(old_b, new_b)
src = src.replace("self.base_opacity = 0.1", "self.base_opacity = 0.3")
f.write_text(src)
EOF
# rmapi binary
COPY backup/rmapi /usr/local/bin/rmapi
RUN chmod +x /usr/local/bin/rmapi
# Skripte
COPY scripts/backup-official-cloud.sh /usr/local/bin/remarkable-backup.sh
COPY scripts/convert-to-pdf.sh /usr/local/bin/remarkable-convert.sh
COPY scripts/merge-annotations.py /usr/local/bin/remarkable-merge.py
RUN chmod +x /usr/local/bin/remarkable-merge.py
RUN chmod +x /usr/local/bin/remarkable-backup.sh /usr/local/bin/remarkable-convert.sh
# Web-UI
COPY web/ /app/
# Entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,128 +0,0 @@
# reMarkable Backup (offizielle Cloud) + lokale Web-UI
Dieses Projekt ist auf einen klaren Workflow reduziert:
- Tablet bleibt in der **offiziellen reMarkable-Cloud** (OCR/Convert bleibt aktiv).
- `rmapi` laeuft im Docker-Container und zieht periodisch Daten aus der offiziellen Cloud.
- `.rmdoc`-Dateien werden automatisch zu **PDF konvertiert** (Originale bleiben erhalten).
- Lokale Ablage im Docker-Volume `remarkable_notizen` mit inkrementellem Stand + datierten Snapshots.
- Web-UI zum Browsen und Oeffnen von PDFs direkt im Browser.
## Dateien in diesem Projekt
```
/opt/remarkable/
├── docker-compose.yml Orchestrierung beider Container
├── backup/
│ ├── Dockerfile Backup-Container (rmapi, rmc, inkscape)
│ └── entrypoint.sh Cron-Setup + Start
├── scripts/
│ ├── backup-official-cloud.sh Backup + Snapshot-Versionierung
│ └── convert-to-pdf.sh .rmdoc → PDF Konvertierung
└── web/
├── Dockerfile Web-UI-Container (PHP)
├── index.php Dateibaum + PDF-Viewer
└── index2.php Archiv-Suche
```
## Voraussetzungen
- Docker + Docker Compose
- Einmaliger reMarkable-Login (Token wird im Volume gespeichert)
## Ersteinrichtung
### 1. Token einrichten
```bash
cd /opt/remarkable
docker compose run --rm --entrypoint bash backup -c "rmapi ls"
# Code von https://my.remarkable.com/device/desktop/connect eingeben
```
Token wird im Volume `remarkable_rmapi-config` gespeichert und bleibt bei Container-Updates erhalten.
### 2. Container starten
```bash
cd /opt/remarkable
docker compose up -d
```
### 3. Erstes Backup manuell ausfuehren
```bash
docker compose exec backup remarkable-backup.sh /data /
```
## Normalbetrieb
Backup + PDF-Konvertierung laufen automatisch per Cron (Standard: taeglich 03:15).
Web-UI erreichbar unter: `http://SERVER_IP:8080`
## Datenstruktur (im Volume `remarkable_notizen`)
```
current/ Aktueller Stand (inkl. konvertierter PDFs)
snapshots/
2026-03-23_031500/
latest -> ...
logs/
```
## Umgebungsvariablen (docker-compose.yml)
| Variable | Standard | Beschreibung |
|----------|----------|--------------|
| `BACKUP_CRON` | `15 3 * * *` | Cron-Zeitplan |
| `SNAPSHOT_KEEP` | `60` | Anzahl aufzubewahrender Snapshots |
| `RUN_ON_START` | `0` | Backup beim Container-Start ausfuehren (`1` = ja) |
## Manuelles Backup mit Optionen
```bash
# Vollsync (alle Dateien neu laden)
docker compose exec backup env BACKUP_FULL=1 remarkable-backup.sh /data /
# Spiegelmodus (lokal geloeschte Dateien entfernen)
docker compose exec backup env BACKUP_MIRROR_DELETE=1 remarkable-backup.sh /data /
```
## PDF-Konvertierung (convert-to-pdf.sh)
Das Skript konvertiert alle `.rmdoc`-Dateien nach dem Backup automatisch zu PDFs.
### Verhalten nach Dokumenttyp
| Typ | Inhalt im rmdoc | Ergebnis |
|-----|-----------------|---------|
| Handschrift-Notiz | `.rm`-Dateien | Seiten werden einzeln via `rmc` + `inkscape` konvertiert und mit `pdfunite` zusammengefuegt |
| Importiertes PDF | `.pdf`-Datei | PDF wird direkt extrahiert |
| Reines EPUB | nur `.epub` | Wird uebersprungen (kein PDF moeglich) |
### Seitenreihenfolge
Die Reihenfolge der Seiten wird aus der `.content`-Datei im rmdoc gelesen. Damit stimmt die Reihenfolge im PDF mit der des Tablets ueberein.
### Bekannte Einschraenkungen
- **Custom ARGB-Farben** (Highlighter, farbige Stifte): werden als grau dargestellt. `rmc` 0.3.0 unterstuetzt keine benutzerdefinierten ARGB-Farbcodes. Issue: https://github.com/ricklupton/rmc/issues
- **Neuere rm-Formate**: Einige `.rm`-Dateien enthalten Daten in einem neueren Format als `rmscene` unterstuetzt — diese Seiten koennen fehlerhaft oder leer erscheinen.
### Konvertierung manuell neu ausfuehren
```bash
# Alle PDFs loeschen und neu konvertieren
docker compose exec backup bash -c "find /data/current -name '*.pdf' -delete && remarkable-convert.sh /data/current"
# Nur neue/geaenderte Dateien konvertieren (Standard)
docker compose exec backup remarkable-convert.sh /data/current
```
## Warum dieser Aufbau
- OCR bleibt verfuegbar, weil die offizielle Cloud weiter genutzt wird.
- Kein Vendor-Lock fuer Aufbewahrung: Daten liegen lokal.
- `.rmdoc`-Originale bleiben erhalten — jederzeit zurueckspielbar.
- Vollstaendig containerisiert: keine Host-Abhaengigkeiten ausser Docker.

View File

@@ -1,22 +0,0 @@
services:
remarkable:
build: .
container_name: remarkable
volumes:
- notizen:/data
- rmapi-config:/root/.config/rmapi
environment:
- BASE_DIR=/data
- BACKUP_CRON=15 3 * * *
- SNAPSHOT_KEEP=60
- RUN_ON_START=0
- RM_BACKUP_ROOT=/data/current
- RM_USER=${RM_USER}
- RM_PASS=${RM_PASS}
ports:
- "8980:8080"
restart: unless-stopped
volumes:
notizen:
rmapi-config:

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${BASE_DIR:-/data}"
BACKUP_CRON="${BACKUP_CRON:-15 3 * * *}"
SNAPSHOT_KEEP="${SNAPSHOT_KEEP:-60}"
RUN_ON_START="${RUN_ON_START:-0}"
mkdir -p "${BASE_DIR}" /var/log
# Cron-Job einrichten
CRON_CMD="SNAPSHOT_KEEP=${SNAPSHOT_KEEP} remarkable-backup.sh ${BASE_DIR} / && remarkable-convert.sh ${BASE_DIR}/current >> /var/log/rm-backup.log 2>&1"
echo "${BACKUP_CRON} root ${CRON_CMD}" > /etc/cron.d/remarkable
chmod 0644 /etc/cron.d/remarkable
echo "== reMarkable Container =="
echo "Cron: ${BACKUP_CRON}"
echo "Base: ${BASE_DIR}"
echo "Retention: ${SNAPSHOT_KEEP}"
if [[ "${RUN_ON_START}" == "1" ]]; then
echo "Starte initiales Backup ..."
SNAPSHOT_KEEP="${SNAPSHOT_KEEP}" remarkable-backup.sh "${BASE_DIR}" / || true
remarkable-convert.sh "${BASE_DIR}/current" || true
fi
# Cron starten
service cron start
# PHP Web-Server im Vordergrund
echo "Web-UI: http://0.0.0.0:8080"
exec php -S 0.0.0.0:8080 -t /app

View File

@@ -1,113 +0,0 @@
#!/usr/bin/env bash
# reMarkable Backup (offizielle Cloud via rmapi)
#
# Ziel:
# - Inkrementeller Download in "current/".
# - Datierte Snapshots in "snapshots/YYYY-MM-DD_HHMMSS/".
# - Versionierung via Hardlinks (unveraenderte Dateien belegen kaum Zusatzplatz).
#
# Nutzung:
# ./backup-official-cloud.sh /www/remarkable/notizen /
#
set -euo pipefail
BASE_DIR="${1:-/www/remarkable/notizen}"
REMOTE_DIR="${2:-/}"
TIMESTAMP="$(date +%F_%H%M%S)"
CURRENT_DIR="${BASE_DIR}/current"
SNAPSHOT_ROOT="${BASE_DIR}/snapshots"
SNAPSHOT_DIR="${SNAPSHOT_ROOT}/${TIMESTAMP}"
LATEST_LINK="${SNAPSHOT_ROOT}/latest"
LOG_DIR="${BASE_DIR}/logs"
LOCK_FILE="${BASE_DIR}/.backup.lock"
# Optional: Pfad, der ein echter Mountpoint sein muss (z. B. /www/remarkable/notizen).
# Wenn gesetzt und kein Mountpoint, wird abgebrochen (sicher gegen "auf Root-FS schreiben").
MOUNT_CHECK_PATH="${MOUNT_CHECK_PATH:-}"
# Optional: Anzahl alter Snapshots behalten (0 = unbegrenzt).
SNAPSHOT_KEEP="${SNAPSHOT_KEEP:-30}"
mkdir -p "${CURRENT_DIR}" "${SNAPSHOT_ROOT}" "${LOG_DIR}"
if [[ -n "${MOUNT_CHECK_PATH}" ]] && ! mountpoint -q "${MOUNT_CHECK_PATH}"; then
echo "Fehler: ${MOUNT_CHECK_PATH} ist nicht gemountet. Backup abgebrochen."
exit 1
fi
if ! command -v rmapi >/dev/null 2>&1; then
echo "Fehler: rmapi nicht im PATH."
exit 1
fi
if ! command -v rsync >/dev/null 2>&1; then
echo "Fehler: rsync fehlt. Bitte installieren."
exit 1
fi
exec 9>"${LOCK_FILE}"
if ! flock -n 9; then
echo "Backup laeuft bereits (Lock: ${LOCK_FILE})."
exit 1
fi
LOG_FILE="${LOG_DIR}/backup-${TIMESTAMP}.log"
exec > >(tee -a "${LOG_FILE}") 2>&1
echo "== reMarkable Backup Start =="
echo "Base: ${BASE_DIR}"
echo "Remote: ${REMOTE_DIR}"
echo "Zeit: ${TIMESTAMP}"
MGET_FLAGS=(-i)
if [[ "${BACKUP_FULL:-}" == "1" ]]; then
MGET_FLAGS=()
fi
if [[ "${BACKUP_MIRROR_DELETE:-}" == "1" ]]; then
MGET_FLAGS+=(-d)
fi
echo "1) Aktualisiere current/ via rmapi mget ${MGET_FLAGS[*]:-(none)} ..."
rmapi mget "${MGET_FLAGS[@]}" -o "${CURRENT_DIR}" "${REMOTE_DIR}"
# Leere Ordner anlegen (rmapi mget erstellt keine leeren Verzeichnisse)
echo "1b) Lege fehlende Ordner an ..."
rmapi find "${REMOTE_DIR}" 2>/dev/null | grep '^\[d\]' | sed 's/^\[d\] *//' | while read -r DIR; do
LOCAL_DIR="${CURRENT_DIR}/${DIR}"
mkdir -p "${LOCAL_DIR}"
done
echo "2) Erzeuge Snapshot: ${SNAPSHOT_DIR}"
mkdir -p "${SNAPSHOT_DIR}"
if [[ -L "${LATEST_LINK}" ]] && [[ -d "$(readlink -f "${LATEST_LINK}")" ]]; then
PREV_SNAPSHOT="$(readlink -f "${LATEST_LINK}")"
rsync -a --delete --link-dest="${PREV_SNAPSHOT}" "${CURRENT_DIR}/" "${SNAPSHOT_DIR}/"
else
rsync -a --delete "${CURRENT_DIR}/" "${SNAPSHOT_DIR}/"
fi
ln -sfn "${SNAPSHOT_DIR}" "${LATEST_LINK}"
if [[ "${SNAPSHOT_KEEP}" =~ ^[0-9]+$ ]] && (( SNAPSHOT_KEEP > 0 )); then
echo "3) Pruefe Retention (keep=${SNAPSHOT_KEEP}) ..."
mapfile -t SNAPSHOTS < <(ls -1 "${SNAPSHOT_ROOT}" | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$' | sort)
if (( ${#SNAPSHOTS[@]} > SNAPSHOT_KEEP )); then
DELETE_COUNT=$(( ${#SNAPSHOTS[@]} - SNAPSHOT_KEEP ))
for OLD in "${SNAPSHOTS[@]:0:DELETE_COUNT}"; do
rm -rf "${SNAPSHOT_ROOT}/${OLD}"
echo " entfernt: ${SNAPSHOT_ROOT}/${OLD}"
done
fi
fi
# PDF-Konvertierung
if command -v remarkable-convert.sh >/dev/null 2>&1; then
echo "4) Konvertiere .rmdoc zu PDF ..."
remarkable-convert.sh "${CURRENT_DIR}"
fi
echo "== Backup fertig =="
echo "Current: ${CURRENT_DIR}"
echo "Snapshot: ${SNAPSHOT_DIR}"

View File

@@ -1,169 +0,0 @@
#!/usr/bin/env bash
# Konvertiert alle .rmdoc-Dateien in /mnt/remarkable/current zu PDFs.
# Originale bleiben erhalten. Bereits konvertierte Dateien werden übersprungen.
#
# Nutzung:
# ./convert-to-pdf.sh [BASE_DIR]
# BASE_DIR: Verzeichnis mit .rmdoc-Dateien (default: /mnt/remarkable/current)
set -euo pipefail
BASE_DIR="${1:-/mnt/remarkable/current}"
TMPDIR_BASE="/tmp/rmdoc_convert"
CONVERTED=0
SKIPPED=0
FAILED=0
if ! command -v rmc >/dev/null 2>&1; then
echo "Fehler: rmc nicht im PATH. Installation: pip3 install rmc --break-system-packages"
exit 1
fi
if ! command -v inkscape >/dev/null 2>&1; then
echo "Fehler: inkscape nicht im PATH. Installation: apt install inkscape"
exit 1
fi
if ! command -v pdfunite >/dev/null 2>&1; then
echo "Fehler: pdfunite nicht im PATH. Installation: apt install poppler-utils"
exit 1
fi
while IFS= read -r -d '' RMDOC; do
PDF="${RMDOC%.rmdoc}.pdf"
# Überspringen falls PDF bereits existiert und neuer als rmdoc
if [[ -f "${PDF}" ]] && [[ "${PDF}" -nt "${RMDOC}" ]]; then
(( SKIPPED++ )) || true
continue
fi
# Primär: rmrl (direkt .rmdoc → PDF, kein Inkscape nötig)
if python3 -m rmrl "${RMDOC}" > "${PDF}" 2>/tmp/rmrl_debug.log && [[ -s "${PDF}" ]]; then
echo "OK: $(basename "${RMDOC}") (rmrl)"
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
(( CONVERTED++ )) || true
rm -rf "${PDF}" # Platzhalter entfernen falls leer
continue
fi
# Primär: rmrl (direkt .rmdoc → PDF, kein Inkscape nötig)
if python3 -m rmrl "${RMDOC}" > "${PDF}" 2>/tmp/rmrl_debug.log && [[ -s "${PDF}" ]]; then
echo "OK: $(basename "${RMDOC}") (rmrl)"
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
(( CONVERTED++ )) || true
continue
fi
TMPDIR="${TMPDIR_BASE}/$$_$(echo "${RMDOC%.rmdoc}" | md5sum | cut -c1-8)"
mkdir -p "${TMPDIR}"
# rmdoc entpacken
if ! unzip -o -q "${RMDOC}" -d "${TMPDIR}" 2>/dev/null; then
echo "WARN: Konnte nicht entpacken: ${RMDOC}"
rm -rf "${TMPDIR}"
(( FAILED++ )) || true
continue
fi
# Seitenreihenfolge aus .content lesen, sonst alphabetisch
RM_FILES=()
CONTENT_FILE="$(find "${TMPDIR}" -maxdepth 1 -name "*.content" | head -1)"
DOC_UUID="$(basename "${CONTENT_FILE%.content}")"
RM_DIR="${TMPDIR}/${DOC_UUID}"
if [[ -f "${CONTENT_FILE}" ]] && [[ -d "${RM_DIR}" ]]; then
# Seiten-UUIDs aus .content in Reihenfolge extrahieren
mapfile -t PAGE_IDS < <(python3 -c "
import json, sys
try:
data = json.load(open('${CONTENT_FILE}'))
pages = data.get('cPages', {}).get('pages', [])
for p in pages:
print(p['id'])
except Exception as e:
sys.exit(1)
" 2>/dev/null)
RM_FILES=()
for PAGE_ID in "${PAGE_IDS[@]}"; do
RM_FILE="${RM_DIR}/${PAGE_ID}.rm"
if [[ -f "${RM_FILE}" ]]; then
RM_FILES+=("${RM_FILE}")
fi
done
fi
# Fallback: alle .rm-Dateien alphabetisch
if [[ ${#RM_FILES[@]} -eq 0 ]]; then
mapfile -t RM_FILES < <(find "${TMPDIR}" -name "*.rm" | sort)
fi
EMBEDDED_PDF="$(find "${TMPDIR}" -maxdepth 2 -name "*.pdf" | head -1)"
if [[ ${#RM_FILES[@]} -eq 0 ]]; then
# Kein Handschrift-Inhalt — eingebettete PDF direkt nutzen
if [[ -n "${EMBEDDED_PDF}" ]]; then
cp "${EMBEDDED_PDF}" "${PDF}"
echo "OK: $(basename "${RMDOC}") (eingebettete PDF)"
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
(( CONVERTED++ )) || true
else
(( SKIPPED++ )) || true
fi
rm -rf "${TMPDIR}"
continue
fi
# Annotiertes Import-Dokument: Annotations auf Original-PDF legen
if [[ -n "${EMBEDDED_PDF}" ]] && [[ ${#RM_FILES[@]} -gt 0 ]]; then
if python3 /usr/local/bin/remarkable-merge.py "${TMPDIR}" "${EMBEDDED_PDF}" "${PDF}" 2>/tmp/merge_debug.log && [[ -s "${PDF}" ]]; then
echo "OK: $(basename "${RMDOC}") (PDF + Annotationen)"
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
(( CONVERTED++ )) || true
else
cp "${EMBEDDED_PDF}" "${PDF}"
echo "WARN: Merge fehlgeschlagen ($(cat /tmp/merge_debug.log | tail -1)), nutze Original-PDF: $(basename "${RMDOC}")"
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
(( CONVERTED++ )) || true
fi
rm -rf "${TMPDIR}"
continue
fi
# Jede Seite einzeln konvertieren, dann zusammenfügen
PAGE_PDFS=()
CONVERT_OK=true
for RM_FILE in "${RM_FILES[@]}"; do
PAGE_PDF="${TMPDIR}/$(basename "${RM_FILE%.rm}").pdf"
if rmc -t pdf -o "${PAGE_PDF}" "${RM_FILE}" 2>/dev/null && [[ -s "${PAGE_PDF}" ]]; then
PAGE_PDFS+=("${PAGE_PDF}")
else
CONVERT_OK=false
break
fi
done
if [[ "${CONVERT_OK}" == true ]] && [[ ${#PAGE_PDFS[@]} -gt 0 ]]; then
if [[ ${#PAGE_PDFS[@]} -eq 1 ]]; then
cp "${PAGE_PDFS[0]}" "${PDF}"
else
pdfunite "${PAGE_PDFS[@]}" "${PDF}" 2>/dev/null
fi
if [[ -s "${PDF}" ]]; then
echo "OK: $(basename "${RMDOC}") (${#PAGE_PDFS[@]} Seiten)"
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
(( CONVERTED++ )) || true
else
rm -f "${PDF}"
echo "WARN: Leere PDF für: $(basename "${RMDOC}")"
(( FAILED++ )) || true
fi
else
rm -f "${PDF}"
echo "WARN: Konvertierung fehlgeschlagen: $(basename "${RMDOC}")"
(( FAILED++ )) || true
fi
rm -rf "${TMPDIR}"
done < <(find "${BASE_DIR}" -name "*.rmdoc" -print0)
echo "== Konvertierung fertig: ${CONVERTED} konvertiert, ${SKIPPED} übersprungen, ${FAILED} fehlgeschlagen =="

View File

@@ -1,182 +0,0 @@
#!/usr/bin/env python3
import sys, os, subprocess, re, shutil, json, tempfile, struct
from pypdf import PdfReader, PdfWriter
INKSCAPE_TIMEOUT = 30
def fallback_page(reader, i, path):
w = PdfWriter()
w.add_page(reader.pages[i])
with open(path, 'wb') as fh:
w.write(fh)
tmpdir = sys.argv[1]
bg_pdf = sys.argv[2]
output = sys.argv[3]
content_file = next(
(os.path.join(tmpdir, f) for f in os.listdir(tmpdir) if f.endswith('.content')), None
)
if not content_file:
sys.exit(1)
with open(content_file) as f:
data = json.load(f)
doc_uuid = os.path.basename(content_file)[:-8]
rm_dir = os.path.join(tmpdir, doc_uuid)
page_ids = [p['id'] for p in data.get('cPages', {}).get('pages', [])]
page_rm = {}
for i, pid in enumerate(page_ids):
rm = os.path.join(rm_dir, pid + '.rm')
if os.path.exists(rm):
page_rm[i] = rm
if not page_rm:
sys.exit(1)
reader = PdfReader(bg_pdf)
n_pages = len(reader.pages)
work = tempfile.mkdtemp()
parts = []
for i in range(n_pages):
part_out = os.path.join(work, 'p' + str(i) + '.pdf')
if i not in page_rm:
fallback_page(reader, i, part_out)
parts.append(part_out)
continue
try:
# PDF-Seitengröße in Punkten
pdf_w_pt = float(reader.pages[i].mediabox.width)
pdf_h_pt = float(reader.pages[i].mediabox.height)
dpi = 150
png_w = int(round(pdf_w_pt * dpi / 72))
png_h = int(round(pdf_h_pt * dpi / 72))
# Bg-Seite als PNG rendern
bg_base = os.path.join(work, 'bg' + str(i))
subprocess.run(
['pdftoppm', '-r', str(dpi), '-png', '-f', str(i+1), '-l', str(i+1),
'-singlefile', bg_pdf, bg_base],
capture_output=True, timeout=20
)
bg_png = bg_base + '.png'
if not os.path.exists(bg_png):
raise FileNotFoundError('PNG fehlt')
# .rm -> SVG
svg_out = os.path.join(work, 'a' + str(i) + '.svg')
r = subprocess.run(['rmc', '-t', 'svg', '-o', svg_out, page_rm[i]],
capture_output=True, timeout=20)
if r.returncode != 0 or not os.path.exists(svg_out):
raise RuntimeError('rmc fehlgeschlagen')
svg = open(svg_out).read()
# Weissen Hintergrund entfernen
svg = re.sub(
r'<rect\b[^>]*/?>',
lambda m: '' if re.search(r'fill\s*[=:]\s*["\']?\s*(?:white|#fff(?:fff)?)', m.group(0), re.I) else m.group(0),
svg
)
# ViewBox X-Offset auslesen
vb = re.search(r'viewBox=["\']([^"\']+)["\']', svg)
vb_vals = [float(x) for x in vb.group(1).split()] if vb else [0, 0, pdf_w_pt, pdf_h_pt]
vb_x = vb_vals[0]
vb_y = vb_vals[1]
# Inneren SVG-Inhalt extrahieren
inner_m = re.search(r'<svg[^>]*>(.*)</svg>', svg, re.DOTALL)
inner = inner_m.group(1) if inner_m else ''
# GlyphRange-Highlights (Textmarkierungen) aus .rm lesen
highlight_svg = ''
try:
import rmscene
RM_SCALE = 72.0 / 226 # rmscene screen-px → SVG-Punkte (wie rmc)
HIGHLIGHT_COLORS = {
9: ('rgb(255,235,0)', 0.4), # HIGHLIGHT (gelb)
3: ('rgb(251,247,25)', 0.4), # YELLOW
4: ('rgb(0,255,0)', 0.4), # GREEN
5: ('rgb(255,192,203)', 0.4), # PINK
6: ('rgb(78,105,201)', 0.4), # BLUE
7: ('rgb(179,62,57)', 0.4), # RED
}
with open(page_rm[i], 'rb') as rmf:
blocks = list(rmscene.read_blocks(rmf))
for block in blocks:
if not (hasattr(block, 'item') and hasattr(block.item, 'value')):
continue
glyph = block.item.value
if not hasattr(glyph, 'rectangles'):
continue
try:
color_id = int(glyph.color)
except Exception:
color_id = 9
fill, opacity = HIGHLIGHT_COLORS.get(color_id, ('rgb(255,235,0)', 0.4))
for rect in glyph.rectangles:
highlight_svg += (
f'<rect x="{rect.x * RM_SCALE:.3f}" y="{rect.y * RM_SCALE:.3f}" '
f'width="{rect.w * RM_SCALE:.3f}" height="{rect.h * RM_SCALE:.3f}" '
f'fill="{fill}" fill-opacity="{opacity}" stroke="none"/>\n'
)
except Exception as e:
print(f' GlyphRange Fehler: {e}', flush=True)
# Highlights VOR den Stift-Annotationen einblenden
if highlight_svg:
print(f' {highlight_svg.count("<rect")} GlyphRect(s) für Seite {i+1}', flush=True)
inner = highlight_svg + inner
# Annotation-Koordinaten sind in PDF-Punkten
# Skalierung: PDF-Punkte -> PNG-Pixel
pts_to_px = png_w / pdf_w_pt
# reMarkable Seitenrand proportional zur PDF-Breite (empirisch: 75.5pt bei A4)
rm_margin_left = 75.5 * pdf_w_pt / 595.275
# reMarkable Seitenrand proportional zur PDF-Breite (empirisch: 75.5pt bei A4)
rm_margin_left = 75.5 * pdf_w_pt / 595.275
# Composite SVG
comp_svg = os.path.join(work, 'c' + str(i) + '.svg')
with open(comp_svg, 'w') as fh:
fh.write('<?xml version="1.0" encoding="UTF-8"?>\n')
fh.write('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"')
fh.write(' width="' + str(pdf_w_pt) + 'pt"')
fh.write(' height="' + str(pdf_h_pt) + 'pt"')
fh.write(' viewBox="0 0 ' + str(png_w) + ' ' + str(png_h) + '">\n')
fh.write(' <image xlink:href="file://' + bg_png + '" href="file://' + bg_png + '"')
fh.write(' width="' + str(png_w) + '" height="' + str(png_h) + '"/>\n')
fh.write(' <g transform="scale(' + str(pts_to_px) + ',' + str(pts_to_px) + ')')
fh.write(' translate(' + str(-vb_x + rm_margin_left) + ',' + str(-vb_y) + ')">\n')
fh.write(' ' + inner + '\n')
fh.write(' </g>\n')
fh.write('</svg>')
# Composite -> PDF
r = subprocess.run(
['inkscape', comp_svg, '--export-type=pdf', '--export-filename=' + part_out],
capture_output=True, timeout=INKSCAPE_TIMEOUT
)
if r.returncode != 0 or not os.path.exists(part_out) or os.path.getsize(part_out) < 100:
raise RuntimeError('inkscape fehlgeschlagen')
parts.append(part_out)
print(' Seite ' + str(i+1) + ' mit Annotation', flush=True)
except Exception as e:
print(' Seite ' + str(i+1) + ' Fallback: ' + str(e), flush=True)
fallback_page(reader, i, part_out)
parts.append(part_out)
if len(parts) == 1:
shutil.copy(parts[0], output)
elif parts:
subprocess.run(['pdfunite'] + parts + [output], check=True)
shutil.rmtree(work)

View File

@@ -1,7 +0,0 @@
FROM php:8.3-cli
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
COPY backup/rmapi /usr/local/bin/rmapi
RUN chmod +x /usr/local/bin/rmapi
WORKDIR /app
COPY web/ /app
CMD ["php", "-S", "0.0.0.0:8080", "-t", "/app"]

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
$user = getenv('RM_USER') ?: 'admin';
$pass = getenv('RM_PASS') ?: '';
if ($pass === '') return; // kein Passwort gesetzt → kein Schutz
// PHP Built-in Server liefert keine PHP_AUTH_* Variablen — Header manuell parsen
$authUser = $_SERVER['PHP_AUTH_USER'] ?? '';
$authPass = $_SERVER['PHP_AUTH_PW'] ?? '';
if ($authUser === '' && isset($_SERVER['HTTP_AUTHORIZATION'])) {
$decoded = base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6));
[$authUser, $authPass] = explode(':', $decoded, 2) + ['', ''];
}
$ok = $authUser === $user
&& hash_equals(hash('sha256', $pass), hash('sha256', $authPass));
if (!$ok) {
header('WWW-Authenticate: Basic realm="reMarkable"');
header('HTTP/1.1 401 Unauthorized');
echo 'Zugriff verweigert.';
exit;
}

View File

@@ -1,852 +0,0 @@
<?php
declare(strict_types=1);
require __DIR__ . '/auth.php';
$RMAPI = getenv('RMAPI_BIN') ?: '/usr/local/bin/rmapi';
$BACKUP = rtrim(getenv('RM_BACKUP_ROOT') ?: '/data/current', '/');
$TMP = '/tmp/rmweb_cache';
@mkdir($TMP, 0700, true);
function h(string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function safePath(string $p): string {
if ($p === '' || $p === '/') return '/';
$parts = array_filter(explode('/', $p), fn($s) => $s !== '' && $s !== '.' && $s !== '..');
return '/' . implode('/', array_values($parts));
}
function rmapiRun(string $bin, array $args): array {
$cmd = $bin;
foreach ($args as $a) $cmd .= ' ' . escapeshellarg((string)$a);
exec($cmd . ' 2>&1', $out, $code);
return ['out' => $out, 'ok' => $code === 0];
}
function addWebDeleted(string $backup, string $path): void {
// Pfad ohne Erweiterung speichern (deckt .pdf und .rmdoc ab)
$clean = preg_replace('/\.(pdf|rmdoc)$/i', '', $path);
$file = dirname(rtrim($backup, '/')) . '/.web_deleted';
file_put_contents($file, $clean . "\n", FILE_APPEND | LOCK_EX);
}
function rmapiRmRecursive(string $bin, string $cloudPath): void {
// rmapi find gibt Pfade relativ zum Parent zurück, z.B.
// find /trash/Scoreblatt → "[f] Scoreblatt/Notebook 4"
// Daher Parent-Pfad vorbauen
$parent = rtrim(dirname($cloudPath), '/');
$out = [];
exec($bin . ' find ' . escapeshellarg($cloudPath) . ' 2>/dev/null', $out);
$files = [];
$dirs = [];
foreach ($out as $line) {
if (preg_match('/^\[(f|d)\] (.+)$/', trim($line), $m)) {
$abs = $parent . '/' . ltrim($m[2], '/');
if ($m[1] === 'f') $files[] = $abs;
else $dirs[] = $abs;
}
}
// Dateien zuerst löschen
foreach ($files as $p) rmapiRun($bin, ['rm', $p]);
// Ordner von tief nach oben löschen
rsort($dirs); // tiefste zuerst via Länge
usort($dirs, fn($a,$b) => strlen($b) - strlen($a));
foreach ($dirs as $p) rmapiRun($bin, ['rm', $p]);
// Den Pfad selbst löschen (falls nicht schon in $dirs)
if (!in_array($cloudPath, $dirs)) rmapiRun($bin, ['rm', $cloudPath]);
}
function listDir(string $backup, string $path, bool $showRmdoc, bool $showTrash = true): array {
$dir = rtrim($backup, '/') . ($path === '/' ? '' : $path);
if (!is_dir($dir)) return [];
$items = [];
foreach (scandir($dir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') continue;
if ($entry === 'trash' && $path === '/' ) continue;
$full = $dir . '/' . $entry;
$lname = strtolower($entry);
$isRmdoc = str_ends_with($lname, '.rmdoc');
$isPdf = str_ends_with($lname, '.pdf');
$isDir = is_dir($full);
if (!$isDir && !$isPdf && !($showRmdoc && $isRmdoc)) continue;
$items[] = [
'name' => $entry,
'type' => $isDir ? 'folder' : 'file',
'size' => is_file($full) ? filesize($full) : 0,
'mtime' => filemtime($full),
'is_pdf' => $isPdf,
'is_rmdoc' => $isRmdoc,
];
}
return $items;
}
function formatSize(int $bytes): string {
if ($bytes < 1024) return $bytes . ' B';
if ($bytes < 1048576) return round($bytes / 1024, 1) . ' KB';
return round($bytes / 1048576, 1) . ' MB';
}
$action = $_POST['action'] ?? $_GET['action'] ?? '';
$path = safePath($_POST['path'] ?? $_GET['path'] ?? '/');
$flash = '';
$flashOk = true;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'delete') {
$target = safePath($_POST['target'] ?? '');
$localTarget = realpath($BACKUP . $target);
$realBackup = realpath($BACKUP);
if ($target !== '/' && $localTarget !== false && $realBackup !== false
&& str_starts_with($localTarget, $realBackup . '/'))
{
$wasDir = is_dir($localTarget);
$cloudTarget = preg_replace('/\.(pdf|rmdoc)$/i', '', $target);
if ($wasDir) {
shell_exec('rm -rf ' . escapeshellarg($localTarget));
clearstatcache(true, $localTarget);
$ok = !is_dir($localTarget);
} else {
$ok = @unlink($localTarget);
if (str_ends_with(strtolower($target), '.pdf')) {
@unlink($BACKUP . substr($target, 0, -4) . '.rmdoc');
@unlink($BACKUP . substr($target, 0, -4) . '.thumb.jpg');
}
}
if ($wasDir) {
rmapiRmRecursive($RMAPI, $cloudTarget);
$rmResult = ['ok' => true];
} else {
$rmResult = rmapiRun($RMAPI, ['rm', $cloudTarget]);
}
if ($ok) {
addWebDeleted($BACKUP, $target);
$flash = 'Gelöscht: ' . $target;
$flashOk = true;
if (!$rmResult['ok']) $flash .= ' (wird beim nächsten Sync erneut entfernt)';
} else {
$flash = 'Fehler beim Löschen.';
$flashOk = false;
}
}
}
if ($action === 'delete_multi') {
$targets = is_array($_POST['targets'] ?? null) ? $_POST['targets'] : [];
$deleted = 0;
$realBackup = realpath($BACKUP);
foreach ($targets as $t) {
$t = safePath($t);
$lt = $t !== '/' ? realpath($BACKUP . $t) : false;
if (!$lt || !$realBackup || !str_starts_with($lt, $realBackup . '/')) continue;
$ct = preg_replace('/\.(pdf|rmdoc)$/i', '', $t);
if (is_dir($lt)) {
shell_exec('rm -rf ' . escapeshellarg($lt));
clearstatcache(true, $lt);
if (!is_dir($lt)) { $deleted++; addWebDeleted($BACKUP, $t); rmapiRmRecursive($RMAPI, $ct); }
} else {
if (@unlink($lt)) { $deleted++; clearstatcache(true, $lt); addWebDeleted($BACKUP, $t); rmapiRun($RMAPI, ['rm', $ct]); }
}
}
$flash = $deleted . ' Element(e) gelöscht.';
$flashOk = true;
}
if ($action === 'mkdir') {
$name = trim($_POST['name'] ?? '');
$newPath = safePath(rtrim($path, '/') . '/' . $name);
if ($name !== '') {
$r = rmapiRun($RMAPI, ['mkdir', $newPath]);
if ($r['ok']) @mkdir($BACKUP . $newPath, 0755, true);
$flash = $r['ok'] ? 'Ordner erstellt: ' . $newPath : 'Fehler: ' . implode(' ', $r['out']);
$flashOk = $r['ok'];
}
}
if ($action === 'upload') {
$name = trim($_FILES['file']['name'] ?? '');
if ($name !== '' && isset($_FILES['file']['tmp_name']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
$safeName = preg_replace('/[^a-zA-Z0-9_\-\. ]/', '_', $name);
$tmpFile = $TMP . '/' . $safeName;
move_uploaded_file($_FILES['file']['tmp_name'], $tmpFile);
$r = rmapiRun($RMAPI, ['put', $tmpFile, rtrim($path, '/')]);
@unlink($tmpFile);
$flash = $r['ok'] ? 'Hochgeladen: ' . $name : 'Fehler: ' . implode(' ', $r['out']);
$flashOk = $r['ok'];
}
}
if ($action === 'move') {
$src = safePath($_POST['src'] ?? '');
$dst = safePath($_POST['dst'] ?? '');
if ($src !== '/' && $dst !== '' && $src !== $dst) {
$strip = fn(string $p) => preg_replace('/\.(pdf|rmdoc)$/i', '', $p);
$r = rmapiRun($RMAPI, ['mv', $strip($src), $strip($dst)]);
if ($r['ok']) {
$localSrc = realpath($BACKUP . $src);
if ($localSrc) @rename($localSrc, $BACKUP . $dst);
$rmdocSrc = realpath($BACKUP . $strip($src) . '.rmdoc');
if ($rmdocSrc) @rename($rmdocSrc, $BACKUP . $strip($dst) . '.rmdoc');
$thumbSrc = realpath($BACKUP . $strip($src) . '.thumb.jpg');
if ($thumbSrc) @rename($thumbSrc, $BACKUP . $strip($dst) . '.thumb.jpg');
}
$flash = $r['ok'] ? 'Verschoben nach: ' . $dst : 'Fehler: ' . implode(' ', $r['out']);
$flashOk = $r['ok'];
}
}
if ($action === 'move_multi') {
$targets = is_array($_POST['targets'] ?? null) ? $_POST['targets'] : [];
$destDir = safePath($_POST['destination'] ?? '');
$moved = 0;
$strip = fn(string $p) => preg_replace('/\.(pdf|rmdoc)$/i', '', $p);
foreach ($targets as $t) {
$t = safePath($t);
if ($t === '/' || $destDir === '') continue;
$name = basename($t);
$dst = safePath($destDir . '/' . $name);
if ($dst === $t) continue;
$r = rmapiRun($RMAPI, ['mv', $strip($t), $strip($dst)]);
if ($r['ok']) {
$lt = realpath($BACKUP . $t);
if ($lt) @rename($lt, $BACKUP . $dst);
$rs = realpath($BACKUP . $strip($t) . '.rmdoc');
if ($rs) @rename($rs, $BACKUP . $strip($dst) . '.rmdoc');
$rt = realpath($BACKUP . $strip($t) . '.thumb.jpg');
if ($rt) @rename($rt, $BACKUP . $strip($dst) . '.thumb.jpg');
$moved++;
}
}
$flash = $moved . ' Element(e) verschoben nach: ' . $destDir;
$flashOk = true;
}
}
if ($action === 'folders') {
header('Content-Type: application/json');
$browsePath = safePath($_GET['path'] ?? '/');
$dir = rtrim($BACKUP, '/') . ($browsePath === '/' ? '' : $browsePath);
$folders = [];
foreach (scandir($dir) ?: [] as $e) {
if ($e === '.' || $e === '..') continue;
if (is_dir($dir . '/' . $e)) $folders[] = $e;
}
echo json_encode(['path' => $browsePath, 'folders' => $folders]);
exit;
}
if ($action === 'view') {
$target = safePath($_GET['target'] ?? '');
$realLocal = realpath($BACKUP . $target);
$realBackup = realpath($BACKUP);
if (!$realLocal || !$realBackup || !str_starts_with($realLocal, $realBackup . '/')) {
http_response_code(403); echo 'Zugriff verweigert.'; exit;
}
if (!is_file($realLocal)) { http_response_code(404); echo 'Nicht gefunden.'; exit; }
$ext = strtolower(pathinfo($realLocal, PATHINFO_EXTENSION));
$mime = match($ext) { 'pdf' => 'application/pdf', 'jpg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', default => 'application/octet-stream' };
header('Content-Type: ' . $mime);
header('Content-Disposition: inline; filename="' . basename($realLocal) . '"');
header('Content-Length: ' . filesize($realLocal));
readfile($realLocal);
exit;
}
$showRmdoc = isset($_GET['rmdoc']) && $_GET['rmdoc'] === '1';
$showTrash = isset($_GET['trash']) && $_GET['trash'] === '1';
$items = listDir($BACKUP, $path, $showRmdoc, $showTrash);
$sort = $_GET['sort'] ?? 'name';
$order = $_GET['order'] ?? 'asc';
usort($items, function ($a, $b) use ($sort, $order) {
if ($a['type'] !== $b['type']) return $a['type'] === 'folder' ? -1 : 1;
$cmp = strnatcasecmp($a['name'], $b['name']);
return $order === 'desc' ? -$cmp : $cmp;
});
$breadcrumbs = [['label' => 'reMarkable', 'path' => '/']];
$parts = array_filter(explode('/', $path));
$acc = '';
foreach ($parts as $p) { $acc .= '/' . $p; $breadcrumbs[] = ['label' => $p, 'path' => $acc]; }
$parentPath = count($parts) > 0 ? safePath(dirname($path)) : '/';
$folders = array_values(array_filter($items, fn($i) => $i['type'] === 'folder'));
$files = array_values(array_filter($items, fn($i) => $i['type'] === 'file'));
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>reMarkable</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='18' fill='%23000'/%3E%3Ctext y='.9em' font-size='72' font-family='Georgia,serif' font-weight='bold' fill='white' x='50%25' text-anchor='middle'%3Erm%3C/text%3E%3C/svg%3E">
<style>
[x-cloak] { display: none; }
.thumb-img { width:100%; height:100%; object-fit:cover; }
.item-card:has(.sel-cb:checked) .sel-wrap { display:block !important; }
</style>
</head>
<body class="bg-zinc-50 text-zinc-900 min-h-screen overflow-x-hidden" style="font-family:-apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;font-size:14px">
<!-- ── Header ─────────────────────────────────────────────────────── -->
<header class="bg-white border-b border-zinc-200 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-5 flex items-center gap-3 h-14">
<div class="flex items-center gap-2 font-semibold text-[15px] shrink-0 text-zinc-800">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-zinc-400"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
reMarkable
</div>
<div class="w-px h-5 bg-zinc-200 shrink-0 hidden sm:block"></div>
<nav class="hidden sm:flex items-center gap-0.5 flex-1 min-w-0 overflow-hidden text-[13px]">
<?php foreach ($breadcrumbs as $i => $bc):
$isLast = $i === count($breadcrumbs) - 1;
$isFirst = $i === 0;
// Auf Mobile: nur erstes + letztes Element anzeigen, Rest ausblenden
$hideClass = (!$isFirst && !$isLast) ? 'hidden sm:flex' : 'flex';
?>
<?php if ($i > 0): ?><span class="text-zinc-300 px-0.5 text-xs <?= (!$isLast) ? 'hidden sm:inline' : '' ?>">/</span><?php endif; ?>
<div class="<?= $hideClass ?> items-center">
<?php if (!$isLast): ?>
<a href="?path=<?= rawurlencode($bc['path']) ?><?= $showRmdoc ? '&rmdoc=1' : '' ?><?= $showTrash ? '&trash=1' : '' ?>"
class="text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 px-2 py-1 rounded-md transition-colors whitespace-nowrap"><?= h($bc['label']) ?></a>
<?php else: ?>
<span class="font-medium text-zinc-900 px-2 py-1 whitespace-nowrap"><?= h($bc['label']) ?></span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</nav>
<div class="flex items-center gap-2 shrink-0">
<a href="setup.php" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[12px] font-medium rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 transition-colors" title="Setup">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></a>
<a href="?path=%2Ftrash" title="Papierkorb"
class="inline-flex items-center px-2 py-1.5 rounded-lg border border-zinc-200 bg-white text-zinc-400 hover:text-zinc-600 transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
</a>
<button onclick="startSync()" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[12px] font-medium rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 transition-colors" title="Sync">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
<span class="hidden sm:inline">Sync</span>
</button>
<button onclick="openModal('mkdirModal')" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[12px] font-medium rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 transition-colors" title="Neuer Ordner">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span class="hidden sm:inline">Ordner</span>
</button>
<button onclick="openModal('uploadModal')" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[12px] font-medium rounded-lg bg-zinc-900 text-white hover:bg-zinc-700 transition-colors" title="Upload">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg>
<span class="hidden sm:inline">Upload</span>
</button>
</div>
</div>
</header>
<!-- ── Main ───────────────────────────────────────────────────────── -->
<main class="max-w-7xl mx-auto px-3 sm:px-5 py-4 sm:py-6">
<?php if ($flash !== ''): ?>
<div class="mb-5 flex items-center gap-2 px-4 py-2.5 rounded-xl text-[13px] <?= $flashOk ? 'bg-emerald-50 border border-emerald-200 text-emerald-800' : 'bg-red-50 border border-red-200 text-red-800' ?>">
<?= $flashOk ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>' : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' ?>
<?= h($flash) ?>
</div>
<?php endif; ?>
<!-- Toolbar -->
<div class="flex items-center gap-2 mb-6">
<div class="flex-1 flex items-center gap-2">
<?php if ($path !== '/'): ?>
<a href="?path=<?= rawurlencode($parentPath) ?><?= $showRmdoc ? '&rmdoc=1' : '' ?><?= $showTrash ? '&trash=1' : '' ?>"
class="inline-flex items-center gap-1 px-3 py-1.5 text-[12px] font-medium rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 transition-colors">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>
Zurück
</a>
<?php endif; ?>
</div>
<span class="text-[12px] text-zinc-400"><?= count($items) ?> Einträge</span>
<a href="?path=<?= rawurlencode($path) ?><?= $showRmdoc ? '' : '&rmdoc=1' ?>"
class="inline-flex items-center gap-2 px-2.5 py-1.5 text-[12px] rounded-lg border transition-colors <?= $showRmdoc ? 'border-blue-300 bg-blue-50 text-blue-700' : 'border-zinc-200 bg-white text-zinc-500 hover:border-zinc-300' ?>">
<span class="inline-block w-7 h-4 rounded-full relative transition-colors <?= $showRmdoc ? 'bg-blue-500' : 'bg-zinc-300' ?>">
<span class="absolute top-0.5 w-3 h-3 bg-white rounded-full shadow transition-all <?= $showRmdoc ? 'left-3.5' : 'left-0.5' ?>"></span>
</span>
rmdoc
</a>
</div>
<?php if (empty($items)): ?>
<div class="text-center py-24 text-zinc-400">
<svg class="mx-auto mb-4 text-zinc-300" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<p class="text-base font-medium text-zinc-500">Keine Dokumente vorhanden</p>
<p class="text-sm mt-1">Backup noch nicht ausgeführt oder Ordner ist leer</p>
</div>
<?php else: ?>
<!-- ── Ordner ──────────────────────────────────────────────────── -->
<?php if (!empty($folders)): ?>
<div class="mb-7">
<p class="text-[11px] font-semibold uppercase tracking-widest text-zinc-400 mb-3">Ordner</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
<?php foreach ($folders as $item):
$itemPath = safePath(rtrim($path, '/') . '/' . $item['name']); ?>
<div class="group relative flex items-center gap-3 px-4 py-3 bg-white rounded-xl border border-zinc-200 hover:border-zinc-300 hover:shadow-sm cursor-pointer transition-all item-card"
onclick="location.href='?path=<?= rawurlencode($itemPath) ?><?= $showRmdoc ? '&rmdoc=1' : '' ?>'">
<div class="sel-wrap hidden group-hover:block shrink-0" onclick="event.stopPropagation()">
<input type="checkbox" class="sel-cb w-4 h-4 rounded accent-zinc-800" data-path="<?= h($itemPath) ?>" data-folder="1"
onchange="toggleItem(<?= h(json_encode($itemPath)) ?>, this)">
</div>
<div class="w-9 h-9 rounded-xl bg-amber-50 flex items-center justify-center shrink-0">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h3.172a2 2 0 0 1 1.414.586l1.414 1.414A2 2 0 0 0 12.414 8H19a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z" fill="#e8d5b7" stroke="#c4a46b" stroke-width="1.5"/></svg>
</div>
<span class="text-[13px] font-medium text-zinc-700 truncate flex-1 min-w-0"><?= h($item['name']) ?></span>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" onclick="event.stopPropagation()">
<button title="Umbenennen"
onclick="openRename(<?= h(json_encode($itemPath)) ?>,<?= h(json_encode($item['name'])) ?>)"
class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button title="Löschen"
onclick="confirmDelete(<?= h(json_encode($itemPath)) ?>,<?= h(json_encode($item['name'])) ?>, true)"
class="p-1.5 rounded-lg text-zinc-400 hover:text-red-600 hover:bg-red-50 transition-colors">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($folders) && !empty($files)): ?>
<div class="border-t border-zinc-200 mb-7"></div>
<?php endif; ?>
<!-- ── Dokumente ───────────────────────────────────────────────── -->
<?php if (!empty($files)): ?>
<div>
<p class="text-[11px] font-semibold uppercase tracking-widest text-zinc-400 mb-3">Dokumente</p>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-2 sm:gap-3">
<?php foreach ($files as $item):
$itemPath = safePath(rtrim($path, '/') . '/' . $item['name']);
$displayName = preg_replace('/\.(pdf|rmdoc)$/i', '', $item['name']);
$thumbLocal = $BACKUP . preg_replace('/\.(pdf|rmdoc)$/i', '.thumb.jpg', $item['name'] === basename($itemPath) ? $itemPath : $itemPath);
$thumbUrl = preg_replace('/\.(pdf|rmdoc)$/i', '.thumb.jpg', $itemPath);
$hasThumb = file_exists($BACKUP . $thumbUrl);
?>
<div class="group relative bg-white rounded-xl border border-zinc-200 hover:border-zinc-300 hover:shadow-md cursor-pointer transition-all overflow-hidden flex flex-col item-card"
onclick="openViewer(<?= h(json_encode($itemPath)) ?>,<?= h(json_encode($displayName)) ?>)">
<!-- Vorschau -->
<div class="relative bg-zinc-100 overflow-hidden" style="aspect-ratio:3/4">
<div class="sel-wrap hidden group-hover:block absolute top-1.5 left-1.5 z-20" onclick="event.stopPropagation()">
<input type="checkbox" class="sel-cb w-4 h-4 rounded accent-zinc-800" data-path="<?= h($itemPath) ?>"
onchange="toggleItem(<?= h(json_encode($itemPath)) ?>, this)">
</div>
<?php if ($hasThumb): ?>
<img src="?action=view&target=<?= rawurlencode($thumbUrl) ?>" alt="" class="thumb-img">
<?php else: ?>
<div class="w-full h-full flex items-center justify-center">
<?php if ($item['is_rmdoc']): ?>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.5"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
<?php else: ?>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Hover-Overlay mit Aktionen -->
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all flex items-start justify-end p-1.5 opacity-0 group-hover:opacity-100">
<div class="flex gap-1" onclick="event.stopPropagation()">
<a href="?action=view&target=<?= rawurlencode($itemPath) ?>" target="_blank"
class="p-1.5 rounded-lg bg-white/90 text-zinc-600 hover:text-zinc-900 shadow-sm transition-colors" title="In neuem Tab öffnen">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</a>
<button onclick="openRename(<?= h(json_encode($itemPath)) ?>,<?= h(json_encode($item['name'])) ?>)"
class="p-1.5 rounded-lg bg-white/90 text-zinc-600 hover:text-zinc-900 shadow-sm transition-colors" title="Umbenennen">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button onclick="confirmDelete(<?= h(json_encode($itemPath)) ?>,<?= h(json_encode($item['name'])) ?>)"
class="p-1.5 rounded-lg bg-white/90 text-red-500 hover:text-red-700 shadow-sm transition-colors" title="Löschen">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
</button>
</div>
</div>
</div>
<!-- Name -->
<div class="px-2.5 py-2">
<p class="text-[12px] font-medium text-zinc-700 truncate leading-tight"><?= h($displayName) ?></p>
<p class="text-[11px] text-zinc-400 mt-0.5">
<?= $item['is_rmdoc'] ? 'rmdoc' : 'PDF' ?>
<?php if ($item['size'] > 0): ?> · <?= formatSize($item['size']) ?><?php endif; ?>
</p>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</main>
<!-- ── Modals ─────────────────────────────────────────────────────── -->
<div id="mkdirModal" class="modal-bg fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm shadow-2xl border border-zinc-200">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-base">Neuer Ordner</h3>
<button onclick="closeModal('mkdirModal')" class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<form method="post" action="?path=<?= rawurlencode($path) ?>">
<input type="hidden" name="action" value="mkdir">
<input type="hidden" name="path" value="<?= h($path) ?>">
<label class="block text-[12px] font-medium text-zinc-500 mb-1.5">Ordnername</label>
<input type="text" name="name" placeholder="Mein Ordner" autofocus required
class="w-full px-3 py-2 border border-zinc-300 rounded-lg text-[13px] focus:outline-none focus:border-zinc-500 focus:ring-2 focus:ring-zinc-200 mb-4">
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('mkdirModal')" class="px-3 py-2 text-[13px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">Abbrechen</button>
<button type="submit" class="px-3 py-2 text-[13px] font-medium rounded-lg bg-zinc-900 text-white hover:bg-zinc-700 transition-colors">Erstellen</button>
</div>
</form>
</div>
</div>
<div id="uploadModal" class="modal-bg fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm shadow-2xl border border-zinc-200">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-base">Dokument hochladen</h3>
<button onclick="closeModal('uploadModal')" class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<form method="post" enctype="multipart/form-data" action="?path=<?= rawurlencode($path) ?>">
<input type="hidden" name="action" value="upload">
<input type="hidden" name="path" value="<?= h($path) ?>">
<label class="block text-[12px] font-medium text-zinc-500 mb-1.5">PDF oder EPUB</label>
<input type="file" name="file" accept=".pdf,.epub" required
class="w-full text-[13px] text-zinc-600 file:mr-3 file:py-1.5 file:px-3 file:rounded-lg file:border-0 file:text-[12px] file:font-medium file:bg-zinc-100 file:text-zinc-700 hover:file:bg-zinc-200 mb-4">
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('uploadModal')" class="px-3 py-2 text-[13px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">Abbrechen</button>
<button type="submit" class="px-3 py-2 text-[13px] font-medium rounded-lg bg-zinc-900 text-white hover:bg-zinc-700 transition-colors">Hochladen</button>
</div>
</form>
</div>
</div>
<div id="deleteModal" class="modal-bg fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm shadow-2xl border border-zinc-200">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-base">Löschen bestätigen</h3>
<button onclick="closeModal('deleteModal')" class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<p id="deleteLabel" class="text-[13px] text-zinc-500 mb-3"></p>
<div id="deleteFolderWarn" class="hidden mb-4 flex items-start gap-2 bg-red-50 border border-red-200 rounded-lg px-3 py-2.5">
<svg class="shrink-0 mt-0.5" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span class="text-[12px] text-red-700">Achtung: Der gesamte Inhalt des Ordners wird ebenfalls unwiderruflich gelöscht!</span>
</div>
<form method="post" action="?path=<?= rawurlencode($path) ?>" onsubmit="showLoading('Wird gelöscht…')">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="path" value="<?= h($path) ?>">
<input type="hidden" name="target" id="deleteTarget">
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('deleteModal')" class="px-3 py-2 text-[13px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">Abbrechen</button>
<button type="submit" class="px-3 py-2 text-[13px] font-medium rounded-lg bg-red-600 text-white hover:bg-red-700 transition-colors">Löschen</button>
</div>
</form>
</div>
</div>
<div id="renameModal" class="modal-bg fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm shadow-2xl border border-zinc-200">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-base">Umbenennen / Verschieben</h3>
<button onclick="closeModal('renameModal')" class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<form method="post" action="?path=<?= rawurlencode($path) ?>">
<input type="hidden" name="action" value="move">
<input type="hidden" name="path" value="<?= h($path) ?>">
<input type="hidden" name="src" id="renameSrc">
<label class="block text-[12px] font-medium text-zinc-500 mb-1.5">Neuer Pfad</label>
<input type="text" name="dst" id="renameDst" placeholder="/Ordner/Dateiname" required
class="w-full px-3 py-2 border border-zinc-300 rounded-lg text-[13px] focus:outline-none focus:border-zinc-500 focus:ring-2 focus:ring-zinc-200 mb-4">
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('renameModal')" class="px-3 py-2 text-[13px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">Abbrechen</button>
<button type="submit" class="px-3 py-2 text-[13px] font-medium rounded-lg bg-zinc-900 text-white hover:bg-zinc-700 transition-colors">Speichern</button>
</div>
</form>
</div>
</div>
<div id="viewerModal" class="modal-bg fixed inset-0 bg-black/50 hidden items-end sm:items-center justify-center z-50 p-0 sm:p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-t-2xl sm:rounded-2xl w-full sm:max-w-4xl shadow-2xl border border-zinc-200 flex flex-col overflow-hidden" style="height:92vh;max-height:92vh">
<div class="flex items-center justify-between px-4 py-3 border-b border-zinc-200 shrink-0">
<span id="viewerTitle" class="font-medium text-[14px] truncate min-w-0 mr-4"></span>
<div class="flex gap-2 shrink-0">
<a id="viewerOpen" href="#" target="_blank" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-[12px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
Neues Tab
</a>
<button onclick="closeModal('viewerModal')" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-[12px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
Schließen
</button>
</div>
</div>
<iframe id="viewerFrame" src="" class="flex-1 border-none w-full"></iframe>
</div>
</div>
<div id="syncModal" class="modal-bg fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-2xl p-6 w-full max-w-lg shadow-2xl border border-zinc-200">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-base">Sync</h3>
<button id="syncClose" onclick="closeModal('syncModal')" class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<pre id="syncOutput" class="bg-zinc-950 text-zinc-300 p-4 rounded-xl text-[12px] leading-relaxed max-h-80 overflow-y-auto whitespace-pre-wrap break-all font-mono">Warte auf Start...</pre>
<div id="syncDone" class="mt-4 hidden">
<button onclick="location.reload()" class="w-full py-2 text-[13px] font-medium rounded-lg bg-zinc-900 text-white hover:bg-zinc-700 transition-colors">Seite neu laden</button>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="hidden fixed inset-0 bg-white/70 backdrop-blur-sm z-[60] flex items-center justify-center">
<div class="bg-white rounded-2xl shadow-2xl border border-zinc-200 px-8 py-6 flex flex-col items-center gap-3">
<svg class="animate-spin text-zinc-400" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
<p id="loadingText" class="text-[13px] font-medium text-zinc-600">Wird verarbeitet…</p>
</div>
</div>
<!-- Mehrfachauswahl: Löschen bestätigen -->
<div id="bulkDeleteModal" class="modal-bg fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm shadow-2xl border border-zinc-200">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-base">Auswahl löschen</h3>
<button onclick="closeModal('bulkDeleteModal')" class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<p id="bulkDeleteLabel" class="text-[13px] text-zinc-500 mb-3"></p>
<div id="bulkDeleteFolderWarn" class="hidden mb-4 flex items-start gap-2 bg-red-50 border border-red-200 rounded-lg px-3 py-2.5">
<svg class="shrink-0 mt-0.5" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span class="text-[12px] text-red-700">Achtung: Ausgewählte Ordner werden mit ihrem gesamten Inhalt gelöscht!</span>
</div>
<div class="flex gap-2 justify-end">
<button onclick="closeModal('bulkDeleteModal')" class="px-3 py-2 text-[13px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">Abbrechen</button>
<button onclick="submitBulkDelete()" class="px-3 py-2 text-[13px] font-medium rounded-lg bg-red-600 text-white hover:bg-red-700 transition-colors">Unwiderruflich löschen</button>
</div>
</div>
</div>
<!-- Mehrfachauswahl: Verschieben -->
<div id="bulkMoveModal" class="modal-bg fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4" style="backdrop-filter:blur(2px)">
<div class="bg-white rounded-2xl w-full max-w-sm shadow-2xl border border-zinc-200 flex flex-col" style="max-height:80vh">
<div class="flex items-center justify-between px-5 pt-5 pb-3 shrink-0">
<h3 class="font-semibold text-base">Zielordner wählen</h3>
<button onclick="closeModal('bulkMoveModal')" class="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<!-- Breadcrumb -->
<div id="folderBreadcrumb" class="flex items-center gap-1 px-5 pb-2 text-[12px] text-zinc-500 shrink-0 flex-wrap"></div>
<!-- Ordner-Liste -->
<div id="folderList" class="flex-1 overflow-y-auto border-t border-b border-zinc-100 divide-y divide-zinc-50 min-h-[120px]"></div>
<!-- Aktuelle Auswahl + Buttons -->
<div class="px-5 py-4 shrink-0">
<p class="text-[11px] text-zinc-400 mb-1">Ziel:</p>
<p id="folderSelected" class="text-[13px] font-medium text-zinc-700 mb-3 truncate">/</p>
<div class="flex gap-2 justify-end">
<button onclick="closeModal('bulkMoveModal')" class="px-3 py-2 text-[13px] font-medium rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 transition-colors">Abbrechen</button>
<button onclick="submitBulkMove()" class="px-3 py-2 text-[13px] font-medium rounded-lg bg-zinc-900 text-white hover:bg-zinc-700 transition-colors">Hierher verschieben</button>
</div>
</div>
</div>
</div>
<!-- Mehrfachauswahl-Bar -->
<div id="selBar" class="hidden fixed bottom-6 left-1/2 -translate-x-1/2 bg-zinc-900 text-white rounded-2xl shadow-2xl px-4 py-3 flex items-center gap-3 z-40">
<label class="flex items-center gap-2 text-[13px] cursor-pointer select-none">
<input type="checkbox" id="selAll" class="w-4 h-4 rounded" onchange="toggleAll(this)">
<span id="selCount">0 ausgewählt</span>
</label>
<button onclick="bulkMove()" class="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded-lg text-[12px] font-medium transition-colors">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><polyline points="12 5 19 12 12 19"/></svg>
Verschieben
</button>
<button onclick="bulkDelete()" class="flex items-center gap-1.5 px-3 py-1.5 bg-red-500 hover:bg-red-600 rounded-lg text-[12px] font-medium transition-colors">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
Löschen
</button>
<button onclick="cancelSelection()" class="p-1.5 rounded-lg hover:bg-zinc-700 transition-colors" title="Abbrechen">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<script>
function openModal(id) {
const el = document.getElementById(id);
el.classList.remove('hidden');
el.classList.add('flex');
}
function closeModal(id) {
const el = document.getElementById(id);
el.classList.add('hidden');
el.classList.remove('flex');
if (id === 'viewerModal') document.getElementById('viewerFrame').src = '';
}
document.querySelectorAll('.modal-bg').forEach(el => {
el.addEventListener('click', e => { if (e.target === el) closeModal(el.id); });
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') document.querySelectorAll('.modal-bg.flex').forEach(el => closeModal(el.id));
});
function confirmDelete(path, name, isFolder) {
document.getElementById('deleteTarget').value = path;
document.getElementById('deleteLabel').textContent = '"' + name + '" wird unwiderruflich gelöscht.';
const warn = document.getElementById('deleteFolderWarn');
if (isFolder) { warn.classList.remove('hidden'); } else { warn.classList.add('hidden'); }
openModal('deleteModal');
}
function openRename(path, name) {
document.getElementById('renameSrc').value = path;
document.getElementById('renameDst').value = path;
openModal('renameModal');
}
function openViewer(path, title) {
const url = '?action=view&target=' + encodeURIComponent(path);
document.getElementById('viewerTitle').textContent = title || path.split('/').pop();
document.getElementById('viewerFrame').src = url;
document.getElementById('viewerOpen').href = url;
openModal('viewerModal');
}
const selected = new Set();
function toggleItem(path, cb) {
if (cb.checked) selected.add(path); else selected.delete(path);
updateSelBar();
}
function toggleAll(cb) {
document.querySelectorAll('.sel-cb').forEach(c => {
c.checked = cb.checked;
if (cb.checked) selected.add(c.dataset.path); else selected.delete(c.dataset.path);
});
updateSelBar();
}
function updateSelBar() {
const n = selected.size;
document.getElementById('selBar').classList.toggle('hidden', n === 0);
document.getElementById('selCount').textContent = n + ' ausgewählt';
}
function cancelSelection() {
selected.clear();
document.querySelectorAll('.sel-cb').forEach(c => c.checked = false);
document.getElementById('selAll').checked = false;
updateSelBar();
}
function showLoading(text) {
document.getElementById('loadingText').textContent = text || 'Wird verarbeitet…';
const ol = document.getElementById('loadingOverlay');
ol.classList.remove('hidden');
ol.classList.add('flex');
}
function buildMultiForm(action) {
const form = document.createElement('form');
form.method = 'post';
form.action = '?path=<?= rawurlencode($path) ?>';
const ai = document.createElement('input'); ai.type='hidden'; ai.name='action'; ai.value=action;
form.appendChild(ai);
selected.forEach(p => {
const i = document.createElement('input'); i.type='hidden'; i.name='targets[]'; i.value=p;
form.appendChild(i);
});
return form;
}
function bulkDelete() {
if (!selected.size) return;
document.getElementById('bulkDeleteLabel').textContent = selected.size + ' Element(e) werden unwiderruflich gelöscht.';
const hasFolder = [...document.querySelectorAll('.sel-cb:checked')].some(cb => cb.dataset.folder === '1');
const warn = document.getElementById('bulkDeleteFolderWarn');
if (hasFolder) { warn.classList.remove('hidden'); } else { warn.classList.add('hidden'); }
openModal('bulkDeleteModal');
}
function submitBulkDelete() {
closeModal('bulkDeleteModal');
showLoading('Wird gelöscht…');
const form = buildMultiForm('delete_multi');
document.body.appendChild(form);
form.submit();
}
let folderBrowsePath = '/';
function folderBrowse(path) {
folderBrowsePath = path;
document.getElementById('folderSelected').textContent = path;
const list = document.getElementById('folderList');
list.innerHTML = '<div class="px-5 py-3 text-[12px] text-zinc-400">Lädt…</div>';
// Breadcrumb aufbauen
const bc = document.getElementById('folderBreadcrumb');
bc.innerHTML = '';
const parts = path === '/' ? [] : path.split('/').filter(Boolean);
const addCrumb = (label, p) => {
if (bc.children.length) { const s=document.createElement('span'); s.textContent='/'; s.className='text-zinc-300'; bc.appendChild(s); }
const a=document.createElement('button'); a.textContent=label; a.className='hover:text-zinc-900 transition-colors';
a.onclick=()=>folderBrowse(p); bc.appendChild(a);
};
addCrumb('reMarkable', '/');
let acc='';
parts.forEach(p => { acc+='/'+p; addCrumb(p, acc); });
fetch('?action=folders&path='+encodeURIComponent(path))
.then(r=>r.json())
.then(data => {
if (!data.folders.length) {
list.innerHTML='<div class="px-5 py-4 text-[12px] text-zinc-400 text-center">Keine Unterordner</div>';
return;
}
list.innerHTML='';
data.folders.forEach(name => {
const fp = (path==='/'?'':path)+'/'+name;
const row=document.createElement('div');
row.className='flex items-center gap-3 px-5 py-2.5 hover:bg-zinc-50 cursor-pointer transition-colors';
row.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 7a2 2 0 0 1 2-2h3.172a2 2 0 0 1 1.414.586l1.414 1.414A2 2 0 0 0 12.414 8H19a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z" fill="#e8d5b7" stroke="#c4a46b" stroke-width="1.5"/></svg>'
+'<span class="text-[13px] text-zinc-700 flex-1">'+name+'</span>'
+'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#a1a1aa" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>';
row.onclick=()=>folderBrowse(fp);
list.appendChild(row);
});
});
}
function bulkMove() {
if (!selected.size) return;
folderBrowse('<?= h($path) ?>');
openModal('bulkMoveModal');
}
function submitBulkMove() {
const dst = folderBrowsePath;
closeModal('bulkMoveModal');
showLoading('Wird verschoben…');
const form = buildMultiForm('move_multi');
const di = document.createElement('input'); di.type='hidden'; di.name='destination'; di.value=dst;
form.appendChild(di);
document.body.appendChild(form);
form.submit();
}
function startSync() {
const output = document.getElementById('syncOutput');
const done = document.getElementById('syncDone');
const close = document.getElementById('syncClose');
output.textContent = '';
done.classList.add('hidden');
close.disabled = true;
openModal('syncModal');
fetch('sync.php')
.then(res => {
const reader = res.body.getReader();
const dec = new TextDecoder();
function read() {
reader.read().then(({ done: d, value }) => {
if (d) { done.classList.remove('hidden'); close.disabled = false; return; }
output.textContent += dec.decode(value);
output.scrollTop = output.scrollHeight;
read();
});
}
read();
})
.catch(e => { output.textContent += '\n[Fehler: ' + e + ']'; close.disabled = false; });
}
</script>
</body>
</html>

View File

@@ -1,157 +0,0 @@
<?php
declare(strict_types=1);
require __DIR__ . '/auth.php';
$backupRoot = getenv('RM_BACKUP_ROOT') ?: '/www/remarkable/notizen/snapshots/latest';
$backupRootReal = realpath($backupRoot);
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function isInsideRoot(string $root, string $path): bool
{
$rootPrefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return str_starts_with($path, $rootPrefix) || $path === $root;
}
$query = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
$selected = isset($_GET['file']) ? (string)$_GET['file'] : '';
$error = '';
if ($backupRootReal === false || !is_dir($backupRootReal)) {
$error = 'Backup-Pfad nicht gefunden: ' . $backupRoot;
}
$files = [];
$selectedPath = null;
if ($error === '') {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$backupRootReal,
FilesystemIterator::SKIP_DOTS
),
RecursiveIteratorIterator::SELF_FIRST
);
$qLower = mb_strtolower($query);
foreach ($iterator as $entry) {
$absolutePath = $entry->getPathname();
$relativePath = ltrim(substr($absolutePath, strlen($backupRootReal)), DIRECTORY_SEPARATOR);
if ($relativePath === '') {
continue;
}
$relativePathLower = mb_strtolower($relativePath);
if ($query !== '' && !str_contains($relativePathLower, $qLower)) {
continue;
}
$files[] = [
'relative' => str_replace(DIRECTORY_SEPARATOR, '/', $relativePath),
'is_dir' => $entry->isDir(),
'size' => $entry->isFile() ? $entry->getSize() : 0,
'mtime' => $entry->getMTime(),
'is_pdf' => $entry->isFile() && str_ends_with(strtolower($relativePath), '.pdf'),
];
}
usort($files, static function (array $a, array $b): int {
if ($a['is_dir'] !== $b['is_dir']) {
return $a['is_dir'] ? -1 : 1;
}
return strnatcasecmp($a['relative'], $b['relative']);
});
if ($selected !== '') {
$candidate = realpath($backupRootReal . DIRECTORY_SEPARATOR . $selected);
if ($candidate !== false && is_file($candidate) && isInsideRoot($backupRootReal, $candidate)) {
$selectedPath = $candidate;
}
}
if (isset($_GET['download']) && $selectedPath !== null) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: inline; filename="' . basename($selectedPath) . '"');
header('Content-Length: ' . (string) filesize($selectedPath));
readfile($selectedPath);
exit;
}
}
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>reMarkable Archiv</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f6f7f9; color: #222; }
.layout { display: grid; grid-template-columns: 430px 1fr; min-height: 100vh; }
.sidebar { border-right: 1px solid #ddd; background: #fff; padding: 14px; overflow: auto; }
.content { padding: 14px; overflow: auto; }
.muted { color: #666; font-size: 12px; }
.item { display: block; padding: 8px 10px; margin: 4px 0; border: 1px solid #eee; border-radius: 8px; text-decoration: none; color: #222; background: #fafafa; }
.item:hover { background: #f0f4ff; border-color: #c9d7ff; }
.dir { font-weight: 600; }
.meta { color: #777; font-size: 12px; margin-top: 4px; }
form { margin-bottom: 12px; }
input[type="text"] { width: 75%; padding: 8px; border: 1px solid #ccc; border-radius: 6px; }
button { padding: 8px 10px; border-radius: 6px; border: 1px solid #bbb; background: #fff; cursor: pointer; }
iframe { width: 100%; height: calc(100vh - 110px); border: 1px solid #ccc; border-radius: 8px; background: #fff; }
.notice { padding: 10px; border: 1px solid #f0c36d; background: #fff8e1; border-radius: 8px; }
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<h2 style="margin: 0 0 8px 0;">reMarkable Archiv</h2>
<div class="muted">Quelle: <?= h($backupRootReal ?: $backupRoot) ?></div>
<form method="get">
<input type="text" name="q" value="<?= h($query) ?>" placeholder="Suche nach Dateinamen oder Ordner...">
<button type="submit">Suchen</button>
</form>
<?php if ($error !== ''): ?>
<div class="notice"><?= h($error) ?></div>
<?php else: ?>
<?php if (count($files) === 0): ?>
<div class="notice">Keine Treffer.</div>
<?php else: ?>
<?php foreach ($files as $entry): ?>
<?php if ($entry['is_dir']): ?>
<div class="item dir">📁 <?= h($entry['relative']) ?></div>
<?php else: ?>
<a class="item" href="?q=<?= rawurlencode($query) ?>&file=<?= rawurlencode($entry['relative']) ?>">
📄 <?= h($entry['relative']) ?>
<div class="meta">
<?= number_format((int) $entry['size'] / 1024, 1, ',', '.') ?> KB ·
<?= h(date('Y-m-d H:i:s', (int) $entry['mtime'])) ?>
<?= $entry['is_pdf'] ? ' · PDF' : '' ?>
</div>
</a>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
</aside>
<main class="content">
<?php if ($selectedPath === null): ?>
<div class="notice">Links eine Datei waehlen. PDFs werden direkt angezeigt.</div>
<?php else: ?>
<?php $rel = ltrim(substr($selectedPath, strlen($backupRootReal)), DIRECTORY_SEPARATOR); ?>
<h3 style="margin-top: 0;"><?= h(str_replace(DIRECTORY_SEPARATOR, '/', $rel)) ?></h3>
<div class="muted" style="margin-bottom: 10px;">
<a href="?q=<?= rawurlencode($query) ?>&file=<?= rawurlencode(str_replace(DIRECTORY_SEPARATOR, '/', $rel)) ?>&download=1">Datei oeffnen/herunterladen</a>
</div>
<?php if (str_ends_with(strtolower($selectedPath), '.pdf')): ?>
<iframe src="?q=<?= rawurlencode($query) ?>&file=<?= rawurlencode(str_replace(DIRECTORY_SEPARATOR, '/', $rel)) ?>&download=1"></iframe>
<?php else: ?>
<div class="notice">Keine PDF-Datei. Bitte ueber den Link oben herunterladen/oeffnen.</div>
<?php endif; ?>
<?php endif; ?>
</main>
</div>
</body>
</html>

View File

@@ -1,118 +0,0 @@
<?php
declare(strict_types=1);
require __DIR__ . '/auth.php';
$RMAPI = '/usr/local/bin/rmapi';
$flash = '';
$flashOk = true;
function h(string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// rmapi-Status prüfen
function rmapiStatus(string $bin): array {
exec($bin . ' ls / 2>&1', $out, $code);
return ['ok' => $code === 0, 'out' => implode("\n", $out)];
}
// Token einrichten
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['code'])) {
$code = trim($_POST['code']);
$code = preg_replace('/[^a-zA-Z0-9]/', '', $code);
if (strlen($code) === 8) {
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = proc_open(escapeshellarg($RMAPI) . ' ls /', $descriptors, $pipes);
if (is_resource($proc)) {
fwrite($pipes[0], $code . "\n");
fclose($pipes[0]);
$out = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($proc);
$flashOk = $exitCode === 0;
$flash = $flashOk ? 'Token erfolgreich eingerichtet!' : 'Fehler: Ungültiger Code oder Verbindungsproblem.';
}
} else {
$flash = 'Code muss genau 8 Zeichen lang sein.';
$flashOk = false;
}
}
$status = rmapiStatus($RMAPI);
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>reMarkable Setup</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='18' fill='%23000'/%3E%3Ctext y='.9em' font-size='72' font-family='Georgia,serif' font-weight='bold' fill='white' x='50%25' text-anchor='middle'%3Erm%3C/text%3E%3C/svg%3E">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f4f4f4;color:#222;min-height:100vh;display:flex;align-items:center;justify-content:center}
.card{background:#fff;border-radius:12px;padding:32px;width:440px;max-width:95vw;box-shadow:0 4px 24px rgba(0,0,0,.1)}
h1{font-size:20px;font-weight:700;margin-bottom:8px}
.subtitle{font-size:14px;color:#666;margin-bottom:24px}
.status{padding:12px 16px;border-radius:8px;font-size:14px;margin-bottom:20px;display:flex;align-items:center;gap:10px}
.status.ok{background:#dcfce7;border:1px solid #86efac;color:#166534}
.status.err{background:#fee2e2;border:1px solid #fca5a5;color:#991b1b}
.flash{padding:10px 16px;border-radius:8px;font-size:14px;margin-bottom:16px}
.flash.ok{background:#dcfce7;border:1px solid #86efac;color:#166534}
.flash.err{background:#fee2e2;border:1px solid #fca5a5;color:#991b1b}
label{display:block;font-size:13px;font-weight:500;margin-bottom:6px;color:#444}
input[type=text]{width:100%;padding:10px 12px;border:1px solid #ccc;border-radius:8px;font-size:16px;letter-spacing:4px;text-align:center;margin-bottom:12px}
.btn{width:100%;padding:10px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#2563eb;color:#fff}
.btn:hover{background:#1d4ed8}
.hint{font-size:13px;color:#666;margin-top:16px;line-height:1.6}
.hint a{color:#2563eb}
.back{display:block;text-align:center;margin-top:20px;font-size:13px;color:#2563eb;text-decoration:none}
</style>
</head>
<body>
<div class="card">
<h1>📄 reMarkable Setup</h1>
<p class="subtitle">Verbindung zur reMarkable Cloud einrichten</p>
<div class="status <?= $status['ok'] ? 'ok' : 'err' ?>">
<?= $status['ok'] ? '✓ Verbunden mit reMarkable Cloud' : '✗ Nicht verbunden — Token fehlt oder abgelaufen' ?>
</div>
<?php if ($flash !== ''): ?>
<div class="flash <?= $flashOk ? 'ok' : 'err' ?>"><?= h($flash) ?></div>
<?php endif; ?>
<?php if (!$status['ok']): ?>
<form method="post">
<label for="code">One-Time-Code</label>
<input type="text" id="code" name="code" maxlength="8" placeholder="xxxxxxxx" autocomplete="off" autofocus>
<button type="submit" class="btn">Verbinden</button>
</form>
<p class="hint">
Code abrufen unter:<br>
<a href="https://my.remarkable.com/device/desktop/connect" target="_blank">
my.remarkable.com/device/desktop/connect
</a>
</p>
<?php else: ?>
<form method="post">
<label for="code">Token erneuern (neuer One-Time-Code)</label>
<input type="text" id="code" name="code" maxlength="8" placeholder="xxxxxxxx" autocomplete="off">
<button type="submit" class="btn btn-secondary" style="background:#6b7280">Token erneuern</button>
</form>
<p class="hint">
Neuen Code abrufen unter:<br>
<a href="https://my.remarkable.com/device/desktop/connect" target="_blank">
my.remarkable.com/device/desktop/connect
</a>
</p>
<?php endif; ?>
<a href="/" class="back">← Zurück zur Übersicht</a>
</div>
</body>
</html>

View File

@@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
require __DIR__ . '/auth.php';
$BASE_DIR = rtrim(getenv('BASE_DIR') ?: '/data', '/');
$BACKUP_SCRIPT = '/usr/local/bin/remarkable-backup.sh';
$CONVERT_SCRIPT= '/usr/local/bin/remarkable-convert.sh';
$SNAPSHOT_KEEP = getenv('SNAPSHOT_KEEP') ?: '60';
// Läuft bereits ein Sync?
$LOCK = $BASE_DIR . '/.backup.lock';
header('Content-Type: text/plain; charset=utf-8');
header('X-Accel-Buffering: no');
header('Cache-Control: no-cache');
if (ob_get_level()) ob_end_clean();
function stream(string $cmd): int {
$handle = popen($cmd . ' 2>&1', 'r');
if ($handle === false) return 1;
while (!feof($handle)) {
$chunk = fread($handle, 256);
if ($chunk !== false && $chunk !== '') {
echo $chunk;
flush();
}
}
return pclose($handle);
}
echo "=== Backup ===\n";
flush();
$exit = stream("SNAPSHOT_KEEP={$SNAPSHOT_KEEP} {$BACKUP_SCRIPT} {$BASE_DIR} /");
if ($exit !== 0) {
echo "\n[FEHLER] Backup fehlgeschlagen (exit {$exit})\n";
exit;
}
// Web-gelöschte Dateien wieder entfernen (kommen durch Sync zurück)
$excludeFile = $BASE_DIR . '/.web_deleted';
if (file_exists($excludeFile)) {
$lines = file($excludeFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$removed = 0;
foreach (array_unique($lines) as $p) {
$p = '/' . ltrim($p, '/');
$local = $BASE_DIR . '/current' . $p;
if (is_dir($local)) {
shell_exec('rm -rf ' . escapeshellarg($local));
$removed++;
} else {
foreach ([$local, $local.'.pdf', $local.'.rmdoc', $local.'.thumb.jpg'] as $f) {
if (is_file($f) && @unlink($f)) $removed++;
}
}
}
if ($removed) echo "\n[Web-Löschung] $removed Element(e) erneut entfernt.\n";
}
echo "\n=== PDF-Konvertierung ===\n";
flush();
stream("{$CONVERT_SCRIPT} {$BASE_DIR}/current");
echo "\n=== Fertig ===\n";

View File

@@ -1,11 +0,0 @@
<h2>Sshwifty</H2>
<p></p>
<p></p>
<img src="https://static-00.iconduck.com/assets.00/sshwifty-icon-2048x2048-pgotb3pw.png" alt="Sshwifty" width="200" />
<p></p>
<p></p>
<p><b>Sshwifty</b> ist ein SSH- und Telnet-Client für das Web, mit dem Sie direkt über Ihren Webbrowser auf SSH- und Telnet-Dienste zugreifen können.
</p>
<p></p>
<a href="https://github.com/nirui/sshwifty" target="_blank">Sshwifty</a>
<p></p>

View File

@@ -1,3 +1,4 @@
version: '3'
services: services:
sshwifty: sshwifty:
image: 'niruix/sshwifty:latest' image: 'niruix/sshwifty:latest'

View File

@@ -1,13 +0,0 @@
<h2>Stirling PDF</H2>
<p></p>
<p></p>
<img src="https://avatars.githubusercontent.com/u/139791695?v=4" alt="Stirling PDF" width="200" />
<p></p>
<p></p>
<p><b>Stirling PDF</b> ist ein robustes, lokal gehostetes webbasiertes PDF-Manipulationstool mit Docker. Es ermöglicht Ihnen, verschiedene Vorgänge an PDF-Dateien durchzuführen, darunter Teilen, Zusammenführen, Konvertieren, Neuorganisieren, Hinzufügen von Bildern, Drehen, Komprimieren und mehr. Diese lokal gehostete Webanwendung verfügt nun über einen umfassenden Satz an Funktionen, die alle Ihre PDF-Anforderungen erfüllen.
Alle Dateien und PDFs sind entweder ausschließlich auf der Clientseite vorhanden, befinden sich nur während der Aufgabenausführung im Serverspeicher oder befinden sich vorübergehend ausschließlich zur Ausführung der Aufgabe in einer Datei. Alle vom Benutzer heruntergeladenen Dateien werden zu diesem Zeitpunkt vom Server gelöscht..
</p>
<p></p>
<a href="https://stirlingtools.com/" target="_blank">Stirling PDF</a>
<p></p>

View File

@@ -1,15 +0,0 @@
services:
stirling_pdf:
image: frooodle/s-pdf:latest
container_name: stirling_pdf
ports:
- 8003:8080
volumes:
- ./trainingData:/usr/share/tesseract-ocr/5/tessdata #Required for extra OCR languages
- ./Configs:/configs
# - /location/of/customFiles:/customFiles/
# - /location/of/logs:/logs/
environment:
- DOCKER_ENABLE_SECURITY=false
restart: unless-stopped

View File

@@ -1 +0,0 @@
PASS=ChangeMe

View File

@@ -1,26 +0,0 @@
<h2>UniFi Network Application</H2>
<p></p>
<p></p>
<img src="https://blog.ui.com/wp-content/uploads/2016/10/unifi-app-logo.png" alt="UniFi Network Application" width="200" />
<p></p>
<p></p>
<p><b>UniFi Network Application</b> ist eine leistungsstarke Wireless-Software-Engine für Unternehmen, die sich ideal für Client-Bereitstellungen mit hoher Dichte eignet, die geringe Latenz und hohe Verfügbarkeitsleistung erforder.
</p>
<p></p>
<a href="https://github.com/linuxserver/docker-unifi-network-application/" target="_blank">UniFi Network Application</a>
<p></p>
<h3>HowTo</h3>
<p></p>
<p><b>* * * A C H T U N G * * *</b></p>
<p>init-mongo.js muss <b>vor dem ersten Start</b> in den Hauptordner kopiert werden. Das Passwort in der Datei muss ebenfalls angepasst werden.</p>
<p></p>
<hr>
<p></p>
<p>Falls die AP sich nicht automatisch verbinden, m&uuml;ssen die AP manuell per SSH angepasst werden&nbsp;</p>
<pre>>ssh
<p></p>
IP des Ger&auml;tes oder Hostname (AP-OG, AP-EG,..)
Username: Borgal
Passwort: *******
set-inform http://docker:8080/inform</pre>

View File

@@ -1,49 +1,18 @@
version: "2.1"
services: services:
unifi-db: unifi-controller:
image: docker.io/mongo:7 image: ghcr.io/linuxserver/unifi-controller
container_name: unifi-db container_name: unifi-controller
volumes:
- ./data:/data/db
- ./data/configdb:/data/configdb
- ./init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro
# healthcheck:
# test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
# interval: 10s
# timeout: 10s
# retries: 5
# start_period: 20s
restart: unless-stopped
unifi-network-application:
image: lscr.io/linuxserver/unifi-network-application:latest
container_name: unifi-network-application
environment: environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin - TZ=Europe/Berlin
- MONGO_USER=unifi - PUID=0
- MONGO_PASS=${PASS} - PGID=0
- MONGO_HOST=unifi-db # - MEM_LIMIT=2048M
- MONGO_PORT=27017
- MONGO_DBNAME=unifi
- MEM_LIMIT=1024 #optional
- MEM_STARTUP=1024 #optional
#- MONGO_TLS= #optional
#- MONGO_AUTHSOURCE= #optional
depends_on:
- unifi-db
# condition: service_healthy
volumes:
- ./config:/config
ports: ports:
- 8443:8443
- 3478:3478/udp - 3478:3478/udp
- 10001:10001/udp - 10001:10001/udp
- 8080:8080 - 8080:8080
# - 1900:1900/udp #optional - 8443:8443
# - 8843:8843 #optional volumes:
# - 8880:8880 #optional - /opt/unifi/config:/config
# - 6789:6789 #optional
# - 5514:5514/udp #optional
restart: unless-stopped restart: unless-stopped

View File

@@ -1 +0,0 @@
db.getSiblingDB("unifi").createUser({user: "unifi", pwd: "ChangeMe", roles: [{role: "dbOwner", db: "unifi"}, {role: "dbOwner", db: "unifi_stat"}]});

View File

@@ -1,13 +0,0 @@
<h2>Uptime Kuma</H2>
<p></p>
<p></p>
<img src="https://static-00.iconduck.com/assets.00/uptime-kuma-icon-1024x940-gabwl61r.png" alt="Uptime Kuma" width="200" />
<p></p>
<p></p>
<p><b>Uptime Kuma</b> ist ein benutzerfreundliches, selbst gehostetes Überwachungstool.
</p>
<p></p>
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" alt="Uptime Kuma" width="500" />
<p></p>
<a href="https://github.com/louislam/uptime-kuma" target="_blank">Uptime Kuma</a>
<p></p>

View File

@@ -1,3 +1,6 @@
---
version: '3.3'
services: services:
uptime-kuma: uptime-kuma:
image: louislam/uptime-kuma:latest image: louislam/uptime-kuma:latest

0
vdr/.env.sample Executable file → Normal file
View File

View File

@@ -1,218 +0,0 @@
<h2>VDR</H2>
<p></p>
<p></p>
<img src="https://raw.githubusercontent.com/lapicidae/vdr-server/master/vdr-logo.svg" alt="VDR" width="200" />
<p></p>
<p></p>
<p><b>VDR</b> ist ein kostenloses (Open Source), nichtkommerzielles Projekt von Klaus Schmidinger zur Erstellung eines digitalen Videorecorders unter Verwendung von Standard-PC-Komponenten. Es ist möglich, mit dem DVB-Standard kompatible digitale TV-Sendungen zu empfangen, aufzuzeichnen und wiederzugeben.
</p>
<p></p>
<a href="https://adguard.com/de/adguard-home/overview.html/" target="_blank">VDR</a>
<p></p>
<h3>HowTo</h3>
<hr>
<p></p>
<h3>Cine-Treiber manuell installieren, falls nicht automatisch erkannt.</h3>
<pre>wget http://linuxsupport.digital-devices.eu/ddinfo.sh</code></pre>
<pre>chmod u+x ddinfo.sh
chown 744 ddinfo.sh</code></pre>
<pre>wget http://linuxsupport.digital-devices.eu/dddvb_build.sh</code></pre>
<pre>chmod u+x dddvb_build.sh
chown 744 dddvb_build.sh</code></pre>
<pre>wget http://linuxsupport.digital-devices.eu/dd_fw_update.sh</code></pre>
<pre>chmod u+x dd_fw_update.sh
chown 744 dd_fw_update.sh</code></pre>
<pre>ddinfo.sh</code></pre>
<pre>dddvb_build.sh</code></pre>
<pre>dd_fw_update.sh</code></pre>
<p></p>
<h3>Schreibrechte f&uuml;r Aufnameverzeichniss hinzuf&uuml;gen</h3>
<p></p>
<pre>docker exec -it vdr-server /bin/bash</code></pre>
<pre>chown 777 /usr/lib/vdr/bin/vdr-recordingaction</code></pre>
<p></p>
<h5>Setup.conf</h5>
<p></p>
<pre>AdaptiveSkipAlternate = 0
AdaptiveSkipInitial = 120
AdaptiveSkipPrevNext = 0
AdaptiveSkipTimeout = 3
AlwaysSortFoldersFirst = 1
AntiAlias = 1
AudioLanguages =
ChannelEntryTimeout = 1000
ChannelInfoPos = 0
ChannelInfoTime = 5
ChannelsWrap = 0
ColorKey0 = 0
ColorKey1 = 1
ColorKey2 = 2
ColorKey3 = 3
CurrentChannel = 1
CurrentDolby = 0
CurrentVolume = 255
DefaultLifetime = 99
DefaultPriority = 50
DefaultSortModeRec = 1
DelTimeshiftRec = 0
DeviceBondings =
DiSEqC = 0
DisplaySubtitles = 0
EmergencyExit = 1
EPGBugfixLevel = 3
EPGLanguages =
EPGLinger = 0
EPGScanTimeout = 5
FoldersInTimerMenu = 1
FontFix = Courier:Bold
FontFixSize = 14
FontFixSizeP = 0.030000
FontOsd = Sans Serif:Bold
FontOsdSize = 15
FontOsdSizeP = 0.031000
FontSml = Sans Serif
FontSmlSize = 13
FontSmlSizeP = 0.028000
InitialChannel = S19.2E-1-1019-10301
InitialVolume = -1
InstantRecordTime = 180
LnbFrequHi = 10600
LnbFrequLo = 9750
LnbSLOF = 11700
MarginStart = 2
MarginStop = 10
MarkInstantRecord = 1
MaxVideoFileSize = 1048570
MenuKeyCloses = 0
MenuScrollPage = 1
MenuScrollWrap = 1
MinEventTimeout = 30
MinUserInactivity = 0
MultiSpeedMode = 0
NameInstantRecord = TITLE EPISODE
NextWakeupTime = 0
NumberKeysForChars = 1
OSDAspect = 1.000000
OSDHeight = 403
OSDHeightP = 0.840000
OSDLanguage =
OSDLeft = 58
OSDLeftP = 0.080000
OSDMessageTime = 1
OSDSkin = sttng
OSDTheme = dark
OSDTop = 38
OSDTopP = 0.080000
OSDWidth = 624
OSDWidthP = 0.870000
PauseAtLastMark = 0
PauseKeyHandling = 2
PauseLifetime = 1
PauseOnMarkJump = 1
PauseOnMarkSet = 0
PausePriority = 10
PositionerLastLon = 0
PositionerSpeed = 15
PositionerSwing = 650
PrimaryDVB = 1
ProgressDisplayTime = 0
RcRepeatDelay = 300
RcRepeatDelta = 100
RecordingDirs = 1
RecordKeyHandling = 2
RecSortingDirection = 0
ResumeID = 0
SetSystemTime = 0
ShowChannelNamesWithSource = 0
ShowInfoOnChSwitch = 1
ShowRemainingTime = 0
ShowReplayMode = 0
SiteLat = 0
SiteLon = 0
SkipEdited = 0
SkipSeconds = 60
SkipSecondsRepeat = 60
SplitEditedFiles = 0
StandardCompliance = 0
SubtitleBgTransparency = 0
SubtitleFgTransparency = 0
SubtitleLanguages =
SubtitleOffset = 0
SVDRPDefaultHost = docker
SVDRPHostName = docker
SVDRPPeering = 1
SVDRPTimeout = 300
TimeoutRequChInfo = 1
TimeSource =
TimeTransponder = 0
UpdateChannels = 5
UseDolbyDigital = 1
UsePositioner = 0
UseSmallFont = 1
UseSubtitle = 1
UseVps = 0
VideoDisplayFormat = 1
VideoFormat = 0
VolumeLinearize = 0
VolumeSteps = 51
VpsMargin = 120
ZapTimeout = 3
epgsearch.UseSearchTimers = 1
live.ChannelGroups = 1,2,3,4,5;6,7,8,9,10;11,12,13,14,15;16,17,18,19,20;21,22,23,24,25;26,27,28,29,30;31,32,33,34,35;36,37,38,39,40;41,42,43,44,45;46,47,48,49,50;51,52,53,54,55;56,57,58,59,60;61,62,63,64,65;66,67,68,69,70;71,72,73,74,75;76,77,78,79,80;81,82,83,84,85;86,87,88,89,90;91,92,93,94,95;96,97,98,99,100;101,102,103,104,105;106,107,108,109,110;111,112,113,114,115;116,117,118,119,120;121,122,123,124,125;126,127,128,129,130;131,132,133,134,135;136,137,138,139,140;141,142,143,144,145;146,147,148,149,150;151,152,153,154,155;156,157,158,159,160;161,162,163,164,165;166,167,168,169,170;171,172,173,174,175;176,177,178,179,180;181,182,183,184,185;186,187,188,189,190;191,192,193,194,195;196,197,198,199,200;201,202,203,204,205;206,207,208,209,210;211,212,213,214,215;216,217,218,219,220;221,222,223,224,225;226,227,228,229,230;231,232,233,234,235;236,237,238,239,240;241,242,243,244,245;246,247,248,249,250;251,252,253,254,255;256,257,258,259,260;261,262,263,264,265;266,267,268,269,270;271,272,273,274,275;276,277,278,279,280;281,282,283,284,285;286,287,288,289,290;291,292,293,294,295;296,297,298,299,300;301,302,303,304,305;306,307,308,309,310;311,312,313,314,315;316,317,318,319,320;321,322,323,324,325;326,327,328,329,330;
live.LastChannel = 0
live.LastSortingMode = dateasc
live.LastWhatsOnListMode = list
live.LocalNetMask = 192.168.0.0/16
live.MarkNewRec = 1
live.ScheduleDuration = 8
live.ScreenShotInterval = 1000
live.ShowChannelsWithoutEPG = 1
live.ShowIMDb = 1
live.ShowInfoBox = 0
live.ShowLogo = 0
live.StartPage = whatsonnow
live.StreamdevPort = 3000
live.StreamdevType = TS
live.StreamVideoOpt0 = ffmpeg -loglevel warning -f mpegts -analyzeduration 1.2M -probesize 5M -i &lt;input&gt; -map 0:v -map 0:a:0 -c:v copy -c:a aac -ac 2
live.StreamVideoOpt1 = ffmpeg -loglevel warning -f mpegts -analyzeduration 1.2M -probesize 5M -i &lt;input&gt; -map 0:v -map 0:a:0 -c:v libx264 -preset ultrafast -crf 23 -tune zerolatency -g 25 -r 25 -c:a aac -ac 2
live.StreamVideoOpt2 = ffmpeg -loglevel warning -f mpegts -analyzeduration 1.2M -probesize 5M -i &lt;input&gt; -map 0:v -map 0:a:0 -c:v libx264 -preset ultrafast -crf 23 -tune zerolatency -g 25 -r 25 -c:a aac -ac 2
live.StreamVideoOpt3 = ffmpeg -loglevel warning -f mpegts -analyzeduration 1.2M -probesize 5M -i &lt;input&gt; -map 0:v -map 0:a:0 -c:v libx264 -preset ultrafast -crf 23 -tune zerolatency -g 25 -r 25 -c:a aac -ac 2
live.Theme = marine
live.UseAjax = 1
live.UseAuth = 0
live.UserdefTimes = 20:15
live.UseStreamdev = 1
markad.DeferredShutdown = 0
markad.Execution = 1
markad.GenIndex = 1
markad.HideMainMenuEntry = 1
markad.IgnoreMargins = 0
markad.Log2Rec = 0
markad.LogoOnly = 1
markad.OSDMessage = 0
markad.SaveInfo = 1
markad.SecondPass = 1
markad.Verbose = 0
markad.whileRecording = 1
markad.whileReplaying = 1
streamdev-server.AllowSuspend = 1
streamdev-server.HideMenuEntry = 0
streamdev-server.HTTPBindIP = 0.0.0.0
streamdev-server.HTTPPriority = 0
streamdev-server.HTTPServerPort = 3000
streamdev-server.HTTPStreamType = 0
streamdev-server.IGMPBindIP = 0.0.0.0
streamdev-server.IGMPClientPort = 1234
streamdev-server.IGMPPriority = 0
streamdev-server.IGMPStreamType = 0
streamdev-server.LiveBufferMs = 500
streamdev-server.LoopPrevention = 0
streamdev-server.MaxClients = 12
streamdev-server.ServerPort = 2004
streamdev-server.StartHTTPServer = 1
streamdev-server.StartIGMPServer = 0
streamdev-server.StartServer = 1
streamdev-server.StartSuspended = 1
streamdev-server.VTPBindIP = 0.0.0.0
streamdev-server.VTPPriority = 1
vnsiserver.TimeshiftBufferDir = /vdr/timeshift</code></pre>

Some files were not shown because too many files have changed in this diff Show More