Compare commits
21 Commits
master
...
a17ade51f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a17ade51f2 | ||
|
|
c80f1723e0 | ||
|
|
87eb4018d9 | ||
|
|
d2de6c27bb | ||
|
|
2d6bde89db | ||
|
|
bd8715b2f4 | ||
|
|
e32a5b206e | ||
|
|
2c0c6c3384 | ||
|
|
e8eb471658 | ||
|
|
c86744ec3b | ||
|
|
5bc35e7fee | ||
|
|
46d106cb38 | ||
|
|
d0bee4a21c | ||
|
|
6405b2864e | ||
|
|
0d4065c89f | ||
|
|
336db8c003 | ||
|
|
807495c460 | ||
|
|
3f392c4674 | ||
|
|
ccd14069d6 | ||
|
|
569d5f25ea | ||
|
|
b9c7b02155 |
20
README.md
20
README.md
@@ -1,17 +1,13 @@
|
||||
<H1>Docker-Compose Files Borgals HomeLab</H1>
|
||||
<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>
|
||||
## Docker-Compose Files Borgals HomeLab
|
||||
|
||||
<H2>Environment Variablen</H2>
|
||||
|
||||
- **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
|
||||
### Environment Variablen
|
||||
|
||||
----
|
||||
- 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
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ dns:
|
||||
port: 53
|
||||
anonymize_client_ip: false
|
||||
ratelimit: 0
|
||||
ratelimit_subnet_len_ipv4: 24
|
||||
ratelimit_subnet_len_ipv6: 56
|
||||
ratelimit_whitelist: []
|
||||
refuse_any: true
|
||||
upstream_dns:
|
||||
@@ -39,7 +37,8 @@ dns:
|
||||
- 8.8.8.8
|
||||
- 8.8.4.4
|
||||
fallback_dns: []
|
||||
upstream_mode: load_balance
|
||||
all_servers: false
|
||||
fastest_addr: false
|
||||
fastest_timeout: 1s
|
||||
allowed_clients: []
|
||||
disallowed_clients: []
|
||||
@@ -68,14 +67,12 @@ dns:
|
||||
bootstrap_prefer_ipv6: false
|
||||
upstream_timeout: 10s
|
||||
private_networks: []
|
||||
use_private_ptr_resolvers: false
|
||||
use_private_ptr_resolvers: true
|
||||
local_ptr_upstreams: []
|
||||
use_dns64: false
|
||||
dns64_prefixes: []
|
||||
serve_http3: false
|
||||
use_http3_upstreams: false
|
||||
serve_plain_dns: true
|
||||
hostsfile_enabled: true
|
||||
tls:
|
||||
enabled: false
|
||||
server_name: ""
|
||||
@@ -92,14 +89,12 @@ tls:
|
||||
private_key_path: ""
|
||||
strict_sni_check: false
|
||||
querylog:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 24h
|
||||
size_memory: 1000
|
||||
enabled: true
|
||||
file_enabled: true
|
||||
statistics:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 24h
|
||||
enabled: true
|
||||
@@ -185,15 +180,16 @@ user_rules:
|
||||
- /^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/
|
||||
- /^rr[0-9]-*sn-[a-z0-9]*-[0-9a-z]{4}\.googlevideo\.com/
|
||||
- '##'
|
||||
- '# PandaSecurity'
|
||||
- '##'
|
||||
- '||eventtrack.pandasecurity.com^$important'
|
||||
- '#####'
|
||||
- '# sonstige sperren'
|
||||
- '#####'
|
||||
- '@@||ad.doubleclick.net^$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'
|
||||
- '@@||fhdi3gj7.r.us-east-1.awstrack.me^$important'
|
||||
- '@@||t.notifications.groupe-pvcp.com^$important'
|
||||
@@ -208,23 +204,8 @@ user_rules:
|
||||
- '@@||as.bild.de^$important'
|
||||
- '@@||click.cptrack.de^$important'
|
||||
- '@@||lavieenrose.com^$important'
|
||||
- '@@||fast.skydeutschland.demdex.net^$client=''ROG'''
|
||||
- '@@||email.golfpost.de^$important'
|
||||
- '@@||adservice.google.com^$important'
|
||||
- '@@||rd.bizrate.com^$important'
|
||||
- '||eventtrack.pandasecurity.com^$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:
|
||||
enabled: false
|
||||
@@ -298,9 +279,6 @@ clients:
|
||||
- 192.168.0.51
|
||||
tags: []
|
||||
upstreams: []
|
||||
uid: 018d7fa4-2a1d-71e0-9a76-f7f93da1e64d
|
||||
upstreams_cache_size: 0
|
||||
upstreams_cache_enabled: false
|
||||
use_global_settings: false
|
||||
filtering_enabled: true
|
||||
parental_enabled: false
|
||||
@@ -325,9 +303,6 @@ clients:
|
||||
- 192.168.0.56
|
||||
tags: []
|
||||
upstreams: []
|
||||
uid: 018d7fa4-2a1d-7d6c-a1d4-2f005d7c17ab
|
||||
upstreams_cache_size: 0
|
||||
upstreams_cache_enabled: false
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
@@ -352,9 +327,6 @@ clients:
|
||||
- 192.168.0.1
|
||||
tags: []
|
||||
upstreams: []
|
||||
uid: 018d7fa4-2a1d-7f8d-95fe-fcfe6d363a29
|
||||
upstreams_cache_size: 0
|
||||
upstreams_cache_enabled: false
|
||||
use_global_settings: false
|
||||
filtering_enabled: true
|
||||
parental_enabled: false
|
||||
@@ -379,9 +351,6 @@ clients:
|
||||
- 192.168.0.59
|
||||
tags: []
|
||||
upstreams: []
|
||||
uid: 018d7fa4-2a1d-7739-bbc2-bf9b979f9913
|
||||
upstreams_cache_size: 0
|
||||
upstreams_cache_enabled: false
|
||||
use_global_settings: false
|
||||
filtering_enabled: true
|
||||
parental_enabled: false
|
||||
@@ -406,9 +375,6 @@ clients:
|
||||
- 192.168.0.98
|
||||
tags: []
|
||||
upstreams: []
|
||||
uid: 018d7fa4-2a1d-741f-9824-85cdb1315521
|
||||
upstreams_cache_size: 0
|
||||
upstreams_cache_enabled: false
|
||||
use_global_settings: false
|
||||
filtering_enabled: true
|
||||
parental_enabled: false
|
||||
@@ -433,9 +399,6 @@ clients:
|
||||
- 192.168.0.58
|
||||
tags: []
|
||||
upstreams: []
|
||||
uid: 018d7fa4-2a1d-70b1-8f0b-e7be87a33423
|
||||
upstreams_cache_size: 0
|
||||
upstreams_cache_enabled: false
|
||||
use_global_settings: false
|
||||
filtering_enabled: true
|
||||
parental_enabled: false
|
||||
@@ -460,63 +423,6 @@ clients:
|
||||
- 192.168.0.42
|
||||
tags: []
|
||||
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
|
||||
filtering_enabled: true
|
||||
parental_enabled: false
|
||||
@@ -541,9 +447,6 @@ clients:
|
||||
- 192.168.0.46
|
||||
tags: []
|
||||
upstreams: []
|
||||
uid: 018d7fa4-2a1d-729a-a02b-bb7bd6c6340f
|
||||
upstreams_cache_size: 0
|
||||
upstreams_cache_enabled: false
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
@@ -563,4 +466,4 @@ os:
|
||||
group: ""
|
||||
user: ""
|
||||
rlimit_nofile: 0
|
||||
schema_version: 28
|
||||
schema_version: 27
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
adguardhome:
|
||||
container_name: adguardhome
|
||||
hostname: adguardhome
|
||||
image: adguard/adguardhome:latest
|
||||
volumes:
|
||||
- ./work:/opt/adguardhome/work
|
||||
- ./conf:/opt/adguardhome/conf
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
- 53:53/tcp
|
||||
- 53:53/udp
|
||||
- 3000:3000/tcp
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,51 +1,5 @@
|
||||
<h2>AdGuard</H2>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<img src="https://ubuntu.com/wp-content/uploads/10b2/adguard-compressor.png" alt="Adguard" width="200" />
|
||||
<p></p>
|
||||
<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>
|
||||
## AdGuard
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -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'
|
||||
@@ -1,12 +1,10 @@
|
||||
version: "2"
|
||||
services:
|
||||
bitwarden:
|
||||
image: vaultwarden/server:latest-alpine
|
||||
image: vaultwarden/server:latest
|
||||
container_name: bitwarden
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
# - ADMIN_TOKEN=${ADMIN_TOKEN} # Wird die Admin-Page nicht gebraucht, bitte auskommentieren
|
||||
volumes:
|
||||
- /opt/bitwarden/bwdata/:/data/
|
||||
ports:
|
||||
- 8084:80
|
||||
restart: always
|
||||
restart: always
|
||||
@@ -1,16 +1,11 @@
|
||||
<h2>Bitwarden/Vaultwarden</H2>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<img src="https://i.pcmag.com/imagery/reviews/05JPSXpKxx9c5oL8wwZMAkX-27..1622837895.png" alt="Bitwarden" width="200" />
|
||||
<p></p>
|
||||
<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.
|
||||
## Bitwarden
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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>
|
||||
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.
|
||||
@@ -1,10 +1 @@
|
||||
<h2>Bookstack</H2>
|
||||
<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>
|
||||
Beschreibung
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "3"
|
||||
services:
|
||||
bookstack:
|
||||
image: ghcr.io/linuxserver/bookstack
|
||||
@@ -5,7 +6,6 @@ services:
|
||||
environment:
|
||||
- APP_URL=https://${DOMAIN}
|
||||
- NGINX_ENABLE_FASTCGI_HTTPS=True
|
||||
- TZ=Europe/Berlin
|
||||
- DB_HOST=bookstack_db
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASS=${DB_PASS}
|
||||
@@ -30,4 +30,4 @@ services:
|
||||
- MYSQL_PASSWORD=${DB_PASS}
|
||||
volumes:
|
||||
- /opt/bookstack:/config
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
8
cloud/.env.sample
Normal file
8
cloud/.env.sample
Normal 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
58
cloud/docker-compose.yml
Normal 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}"
|
||||
@@ -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ü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ö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äß funktionieren.<br>Sie können diejenigen, die dies nicht tun, manuell neu installieren.</p>
|
||||
<p></p>
|
||||
@@ -1,3 +1,5 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
code-server:
|
||||
image: ghcr.io/linuxserver/code-server
|
||||
@@ -26,6 +28,6 @@ volumes:
|
||||
name: www
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs4
|
||||
type: nfs
|
||||
o: addr=${NFS_SERVER},rw
|
||||
device: ":${PFAD}"
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: '3.5'
|
||||
services:
|
||||
drawio:
|
||||
image: jgraph/drawio
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
PASS=ChangeMe
|
||||
NFS_SERVER=192.168.0.xxx # oder hostname oder Domain
|
||||
PFAD=/Pfad/zum/Ordner
|
||||
@@ -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>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "2.1"
|
||||
services:
|
||||
duplicati:
|
||||
image: ghcr.io/linuxserver/duplicati
|
||||
@@ -6,11 +7,10 @@ services:
|
||||
- PUID=0
|
||||
- PGID=0
|
||||
- TZ=Europe/Berlin
|
||||
- DUPLICATI__WEBSERVICE_PASSWORD=${PASS}
|
||||
- CLI_ARGS= #optional
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- backup:/backups
|
||||
- Immich:/mnt/immich
|
||||
- /opt:/source
|
||||
- ./shared:/shared
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
@@ -24,14 +24,6 @@ volumes:
|
||||
name: backup
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs4
|
||||
type: nfs
|
||||
o: addr=${NFS_SERVER},rw
|
||||
device: ":${PFAD}"
|
||||
|
||||
Immich:
|
||||
name: Immich
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs4
|
||||
o: addr=${NFS_SERVER},rw
|
||||
device: ":${PFAD2}"
|
||||
|
||||
8
filerun/.env.sample
Normal file
8
filerun/.env.sample
Normal 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
|
||||
45
filerun/docker-compose.yml
Normal file
45
filerun/docker-compose.yml
Normal 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}"
|
||||
37
fredbet/docker-compose.yml
Normal file
37
fredbet/docker-compose.yml
Normal 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
|
||||
@@ -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>
|
||||
@@ -1,3 +1,5 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea
|
||||
@@ -5,7 +7,7 @@ services:
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
restart: unless-stopped
|
||||
restart: always
|
||||
volumes:
|
||||
- ./gitea:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "2"
|
||||
services:
|
||||
gotify:
|
||||
container_name: gotify
|
||||
|
||||
@@ -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>
|
||||
@@ -1,36 +1,21 @@
|
||||
version: '3'
|
||||
services:
|
||||
influxdb:
|
||||
#image: influxdb
|
||||
image: influxdb:1.6.4
|
||||
container_name: influxdb
|
||||
ports:
|
||||
- 8096:8086
|
||||
#- 8098:8088
|
||||
volumes:
|
||||
- ./influxdb:/var/lib/influxdb
|
||||
environment:
|
||||
- INFLUXDB_DB=METRICS
|
||||
- INFLUXDB_USERNAME=${INFLUXDB_USERNAME}
|
||||
- INFLUXDB_PASSWORD=${INFLUXDB_PASSWORD}
|
||||
- INFLUXDB_REPORTING_DISABLED=true
|
||||
#Determines which level of logs will be emitted. The available levels are error, warn, info, and debug.
|
||||
- INFLUXDB_LOGGING_LEVEL=warn
|
||||
networks:
|
||||
- external_network
|
||||
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:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
@@ -44,4 +29,10 @@ services:
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_USERNAME}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
|
||||
networks:
|
||||
- external_network
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
external_network:
|
||||
external: true
|
||||
|
||||
@@ -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>
|
||||
@@ -1,3 +1,5 @@
|
||||
---
|
||||
version: "2.1"
|
||||
services:
|
||||
grocy:
|
||||
image: lscr.io/linuxserver/grocy
|
||||
@@ -10,4 +12,4 @@ services:
|
||||
- ./config:/config
|
||||
ports:
|
||||
- 8011:80
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
@@ -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>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "3"
|
||||
services:
|
||||
heimdall:
|
||||
image: ghcr.io/linuxserver/heimdall
|
||||
@@ -10,4 +11,4 @@ services:
|
||||
- /opt/heimdall/data:/config
|
||||
ports:
|
||||
- 8124:80
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +1,4 @@
|
||||
version: '3'
|
||||
services:
|
||||
homeassistant:
|
||||
container_name: homeassistant
|
||||
@@ -7,15 +8,12 @@ services:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /dev/serial/by-id:/dev/serial/by-id
|
||||
devices:
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
- /dev/ttyUSB0:/dev/ttyUSB0
|
||||
- /dev/ttyACM0:/dev/ttyACM0
|
||||
ports:
|
||||
- 8123:8123
|
||||
- "192.168.0.111:8123:8123"
|
||||
privileged: true
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
# labels:
|
||||
# - "com.centurylinklabs.watchtower.monitor-only=true"
|
||||
restart: unless-stopped
|
||||
@@ -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>
|
||||
@@ -1,3 +1,5 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
hp-scan:
|
||||
image: manuc66/node-hp-scan-to:latest
|
||||
@@ -5,9 +7,8 @@ services:
|
||||
hostname: TrueNAS
|
||||
environment:
|
||||
- IP=192.168.0.11
|
||||
- Label=TrueNas-Scan
|
||||
- PATTERN="scan"_dd.mm.yyyy_hh:MM:ss
|
||||
- PGID=1001
|
||||
- PGID=1000
|
||||
- PUID=1000
|
||||
- TZ=Europe/Berlin
|
||||
labels:
|
||||
@@ -21,6 +22,6 @@ volumes:
|
||||
name: scan
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs4
|
||||
type: nfs
|
||||
o: addr=${NFS_SERVER},rw
|
||||
device: ":${PFAD}"
|
||||
device: ":${PFAD}"
|
||||
@@ -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>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "2"
|
||||
services:
|
||||
imapfilter:
|
||||
build: .
|
||||
@@ -7,4 +8,4 @@ services:
|
||||
- ./config.lua:/volume/configuration/config.lua:ro
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "false"
|
||||
restart: always
|
||||
restart: always
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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}"
|
||||
@@ -1,5 +0,0 @@
|
||||
NFS_SERVER=192.168.0.xxx
|
||||
PFAD=/Pfad/zu/den/Videos
|
||||
#
|
||||
# Auf Rechte achten
|
||||
#
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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ü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ötigt ca. 1 Minute bis sie lä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]¸
|
||||
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>
|
||||
@@ -1,18 +1,13 @@
|
||||
version: "2"
|
||||
services:
|
||||
knxd:
|
||||
image: henfri/knxd:v0.12.6
|
||||
container_name: knxd
|
||||
network_mode: "host"
|
||||
logging:
|
||||
options:
|
||||
max-file: '2'
|
||||
max-size: 10m
|
||||
restart: always
|
||||
|
||||
#
|
||||
# mit logging, wenn es zu Problemen kommt
|
||||
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
|
||||
# mit logging, wenn es zu Problemen kommt - zum aktivieren muss der Container entfernt werden!!!
|
||||
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
|
||||
#
|
||||
# command: knxd -e 1.1.250 -E 1.1.210:8 -DTRS -c -B single --send-delay=30 -b ipt:192.168.0.200
|
||||
# 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
|
||||
|
||||
@@ -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>
|
||||
@@ -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=
|
||||
@@ -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>
|
||||
@@ -1,3 +1,5 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
murmur:
|
||||
image: vimagick/murmur
|
||||
@@ -7,4 +9,4 @@ services:
|
||||
ports:
|
||||
- "64738:64738/tcp"
|
||||
- "64738:64738/udp"
|
||||
restart: always
|
||||
restart: always
|
||||
@@ -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>
|
||||
@@ -1,3 +1,5 @@
|
||||
version: '2.1'
|
||||
|
||||
services:
|
||||
nodered:
|
||||
container_name: node-red
|
||||
@@ -13,4 +15,4 @@ services:
|
||||
#- PGID=1000
|
||||
#- PUID=1000
|
||||
restart: always
|
||||
network_mode: host
|
||||
network_mode: host
|
||||
@@ -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>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: '3'
|
||||
services:
|
||||
onlyoffice-document-server:
|
||||
container_name: onlyoffice-document-server
|
||||
@@ -12,8 +13,4 @@ services:
|
||||
volumes:
|
||||
- ./document_log:/var/log/onlyoffice
|
||||
- ./document_data:/var/www/onlyoffice/Data
|
||||
- ./document_lib:/var/lib/onlyoffice
|
||||
- ./fonts:/usr/share/fonts/truetype/custom
|
||||
- ./redis:/var/lib/redis
|
||||
- ./db:/var/lib/postgresql
|
||||
- ./rabbitmq:/var/lib/rabbitmq
|
||||
- ./document_lib:/var/lib/onlyoffice
|
||||
26
openhab/docker-compose.yml
Normal file
26
openhab/docker-compose.yml
Normal 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
|
||||
@@ -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>
|
||||
@@ -1,3 +1,5 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
owncloud:
|
||||
image: owncloud/server
|
||||
@@ -21,17 +23,17 @@ services:
|
||||
- OWNCLOUD_MYSQL_UTF8MB4=true
|
||||
- OWNCLOUD_REDIS_ENABLED=true
|
||||
- OWNCLOUD_REDIS_HOST=redis
|
||||
# healthcheck:
|
||||
# test: ["CMD", "/usr/bin/healthcheck"]
|
||||
# interval: 30s
|
||||
# timeout: 10s
|
||||
# retries: 5
|
||||
healthcheck:
|
||||
test: ["CMD", "/usr/bin/healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./files:/mnt/data
|
||||
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.11
|
||||
image: mariadb:10.6 # minimum required ownCloud version is 10.9
|
||||
container_name: owncloud_mariadb
|
||||
restart: always
|
||||
environment:
|
||||
@@ -59,4 +61,4 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./redis:/data
|
||||
- ./redis:/data
|
||||
@@ -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!
|
||||
@@ -1,8 +1,8 @@
|
||||
# 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
|
||||
# consumption directory.
|
||||
USERMAP_UID=1000
|
||||
USERMAP_GID=1001
|
||||
#USERMAP_UID=1000
|
||||
#USERMAP_GID=1001
|
||||
|
||||
# Additional languages to install for text recognition, separated by a
|
||||
# whitespace. Note that this is
|
||||
@@ -12,7 +12,7 @@ USERMAP_GID=1001
|
||||
# default.
|
||||
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
|
||||
# for available languages.
|
||||
# PAPERLESS_OCR_LANGUAGES=tur ces
|
||||
#PAPERLESS_OCR_LANGUAGES=tur ces
|
||||
|
||||
###############################################################################
|
||||
# Paperless-specific settings #
|
||||
@@ -24,7 +24,7 @@ USERMAP_GID=1001
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
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
|
||||
# documents are written in.
|
||||
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_CONSUMER_POLLING=10
|
||||
PAPERLESS_CONSUMER_POLLING=30
|
||||
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
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -48,52 +49,34 @@ services:
|
||||
- "com.centurylinklabs.watchtower.monitor-only=true"
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx
|
||||
container_name: paperless_app
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
- broker
|
||||
- gotenberg
|
||||
- tika
|
||||
ports:
|
||||
- 8009:8000/tcp
|
||||
- 8009:8000
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
- ./export:/usr/src/paperless/export
|
||||
- ./consume:/usr/src/paperless/consume
|
||||
- ./scripts:/usr/src/paperless/scripts
|
||||
- media:/usr/src/paperless/media
|
||||
env_file: docker-compose.env
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
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:
|
||||
media:
|
||||
name: media
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs4
|
||||
type: nfs
|
||||
o: addr=${NFS_SERVER},rw
|
||||
device: ":${PFAD}"
|
||||
device: ":${PFAD}"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -x
|
||||
|
||||
# Remove blank pages
|
||||
/usr/src/paperless/scripts/remove-blank-pages.sh
|
||||
@@ -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
4
photoview/.env.sample
Normal file
@@ -0,0 +1,4 @@
|
||||
DB_PASS=ChangeMe
|
||||
|
||||
NFS_SERVER=192.168.0.x
|
||||
PFAD=/mnt/Pfad/zuden/Bildern
|
||||
62
photoview/docker-compose.yml
Normal file
62
photoview/docker-compose.yml
Normal 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}"
|
||||
@@ -1,11 +1,2 @@
|
||||
<h2>Portainer</H2>
|
||||
<p></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>
|
||||
<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 id="bkmrk-docker-hub"><a href="https://hub.docker.com/r/portainer/portainer" target="_blank" rel="noopener">Docker-Hub</a></p>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "2"
|
||||
services:
|
||||
portainer:
|
||||
container_name: portainer
|
||||
@@ -8,4 +9,4 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /opt/portainer:/data
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
7
ports.md
7
ports.md
@@ -13,16 +13,13 @@ wireshark | 3010
|
||||
vdr | 3020
|
||||
gitea | 3030
|
||||
grafana | 3090
|
||||
jdownloader | 3129
|
||||
fredbed_db | 3307
|
||||
webserver_db | 3309
|
||||
unifi controller | 3478
|
||||
jdownloader | 5800
|
||||
vdr | 6419
|
||||
bookstack | 6875
|
||||
portainer | 8000
|
||||
gotify | 8001
|
||||
dockge | 8002
|
||||
wekan | 8004
|
||||
vdr | 8008
|
||||
paperless | 8009
|
||||
@@ -49,10 +46,8 @@ duplicati | 8200
|
||||
openhab | 8444
|
||||
youTubeDL | 8998
|
||||
portainer | 9000
|
||||
mealie | 9925
|
||||
dozzle | 9999
|
||||
unifi controller | 10001
|
||||
vdr-vnsi | 34890
|
||||
wireguard | 51820
|
||||
wireguard | 51821
|
||||
mumble | 64738
|
||||
mumble | 64738
|
||||
@@ -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>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: 'jc21/nginx-proxy-manager:latest'
|
||||
@@ -11,4 +12,4 @@ services:
|
||||
- '443:443'
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./letsencrypt:/etc/letsencrypt
|
||||
- ./letsencrypt:/etc/letsencrypt
|
||||
@@ -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.7.0" "pypdf" "rmrl" --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"]
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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 =="
|
||||
@@ -1,142 +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 ''
|
||||
|
||||
# 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)
|
||||
@@ -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"]
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
@@ -1,3 +1,4 @@
|
||||
version: '3'
|
||||
services:
|
||||
sshwifty:
|
||||
image: 'niruix/sshwifty:latest'
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
PASS=ChangeMe
|
||||
@@ -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üssen die AP manuell per SSH angepasst werden </p>
|
||||
<pre>>ssh
|
||||
<p></p>
|
||||
IP des Gerätes oder Hostname (AP-OG, AP-EG,..)
|
||||
Username: Borgal
|
||||
Passwort: *******
|
||||
|
||||
set-inform http://docker:8080/inform</pre>
|
||||
@@ -1,49 +1,18 @@
|
||||
version: "2.1"
|
||||
services:
|
||||
unifi-db:
|
||||
image: docker.io/mongo:7
|
||||
container_name: unifi-db
|
||||
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
|
||||
unifi-controller:
|
||||
image: ghcr.io/linuxserver/unifi-controller
|
||||
container_name: unifi-controller
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Berlin
|
||||
- MONGO_USER=unifi
|
||||
- MONGO_PASS=${PASS}
|
||||
- MONGO_HOST=unifi-db
|
||||
- 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
|
||||
- PUID=0
|
||||
- PGID=0
|
||||
# - MEM_LIMIT=2048M
|
||||
ports:
|
||||
- 8443:8443
|
||||
- 3478:3478/udp
|
||||
- 10001:10001/udp
|
||||
- 8080:8080
|
||||
# - 1900:1900/udp #optional
|
||||
# - 8843:8843 #optional
|
||||
# - 8880:8880 #optional
|
||||
# - 6789:6789 #optional
|
||||
# - 5514:5514/udp #optional
|
||||
- 8443:8443
|
||||
volumes:
|
||||
- /opt/unifi/config:/config
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
db.getSiblingDB("unifi").createUser({user: "unifi", pwd: "ChangeMe", roles: [{role: "dbOwner", db: "unifi"}, {role: "dbOwner", db: "unifi_stat"}]});
|
||||
@@ -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>
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:latest
|
||||
@@ -6,4 +9,4 @@ services:
|
||||
- ./config:/app/data
|
||||
ports:
|
||||
- "8086:3001"
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
0
vdr/.env.sample
Executable file → Normal file
0
vdr/.env.sample
Executable file → Normal file
218
vdr/README.md
218
vdr/README.md
@@ -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ür Aufnameverzeichniss hinzufü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 <input> -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 <input> -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 <input> -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 <input> -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
Reference in New Issue
Block a user