Compare commits
4 Commits
def2120d9b
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| b54fe0cd60 | |||
| 2c02ed4df3 | |||
| e3339933ba | |||
| a15d27ce14 |
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Runtime-Dateien (nicht ins Repo)
|
||||
config.json
|
||||
state.json
|
||||
rclone.conf
|
||||
logs/
|
||||
|
||||
# Deploy-Script (enthält ggf. Zugangsdaten)
|
||||
deploy.sh
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
venv/
|
||||
.venv/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Mount-Verzeichnisse (auf dem Pi)
|
||||
/mnt/picopy*/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Tobias Leuschner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
315
README.md
Normal file
315
README.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# PiCopy
|
||||
|
||||
**Automatische USB-Backup-Station für den Raspberry Pi mit Web-Interface**
|
||||
|
||||
PiCopy verwandelt deinen Raspberry Pi in ein eigenständiges Backup-Gerät. Stecke eine Quell-USB (Speicherkarte, USB-Stick) und ein Ziel-Laufwerk ein – PiCopy kopiert die Daten automatisch, organisiert sie in Datumsordnern, prüft die Integrität und kann danach auf ein NAS hochladen.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
| | Feature | Beschreibung |
|
||||
|---|---|---|
|
||||
| 📋 | **Automatisches Kopieren** | Startet sofort wenn Quelle und Ziel eingesteckt werden |
|
||||
| 🌐 | **Web-Interface** | Konfiguration und Status von jedem Gerät im Netzwerk |
|
||||
| 📂 | **Datei-Explorer** | Inhalt verbundener Laufwerke direkt im Browser durchsuchen |
|
||||
| 🗂️ | **Smarte Organisation** | Erstellt automatisch Datumsordner (`2024-01-15_143022/`) |
|
||||
| 🔍 | **Dateifilter** | Nur Fotos, nur Videos oder beliebige Dateitypen kopieren |
|
||||
| 🔄 | **Duplikat-Behandlung** | Überspringen / Überschreiben / Umbenennen |
|
||||
| ✅ | **MD5-Verifizierung** | Jede Datei nach dem Kopieren auf Integrität prüfen |
|
||||
| 🗑️ | **Quelle leeren** | Quelldateien nach erfolgreichem Kopieren löschen (Move-Modus) |
|
||||
| 🖧 | **NAS / SMB Upload** | Nach dem lokalen Backup auf ein Netzlaufwerk hochladen |
|
||||
| 📡 | **WiFi-Fallback** | Erstellt einen eigenen Hotspot wenn kein WLAN verfügbar ist |
|
||||
| ⚡ | **Headless-Betrieb** | Kein Monitor, keine Tastatur nötig |
|
||||
| 🔁 | **Autostart** | Startet automatisch beim Pi-Boot via systemd |
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Raspberry Pi (2, 3, 4, 5 oder Zero 2W)
|
||||
- Raspberry Pi OS **Bookworm** (Debian 12) oder neuer
|
||||
- Mindestens 2 USB-Ports (Quelle + Ziel)
|
||||
- WLAN oder LAN für das Web-Interface
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A – Direkt vom Git-Repo (empfohlen)
|
||||
|
||||
```bash
|
||||
git clone https://git.leuschner.dev/Tobias/PiCopy
|
||||
cd PiCopy
|
||||
sudo bash install.sh
|
||||
```
|
||||
|
||||
### Option B – One-Liner
|
||||
|
||||
```bash
|
||||
curl -sSL https://git.leuschner.dev/Tobias/PiCopy/raw/branch/main/install.sh | sudo bash
|
||||
```
|
||||
|
||||
Nach der Installation ist das Web-Interface unter folgender Adresse erreichbar:
|
||||
|
||||
```
|
||||
http://<raspberry-pi-ip>:8080
|
||||
```
|
||||
|
||||
Die IP-Adresse wird am Ende der Installation angezeigt.
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart
|
||||
|
||||
1. **Web-Interface öffnen** → `http://<pi-ip>:8080`
|
||||
2. **USB-Gerät einstecken** das als Quelle dienen soll
|
||||
3. **Port konfigurieren** → Unter *USB Port Konfiguration* das Gerät in der Dropdown-Liste wählen → *Als feste Quelle speichern*
|
||||
4. **Ziel konfigurieren** → Ziel-Laufwerk einstecken, Port wählen → *Als festes Ziel speichern*
|
||||
5. **Auto-Kopie aktivieren** → Haken bei *Automatisch kopieren wenn Quelle & Ziel verbunden*
|
||||
6. **Fertig** → Jetzt beide Laufwerke einstecken → Kopie startet automatisch
|
||||
|
||||
> **Wichtig:** PiCopy merkt sich den **physischen Port** (Anschluss), nicht das spezifische Gerät. Ein anderer USB-Stick im gleichen Anschluss wird automatisch als Quelle/Ziel erkannt.
|
||||
|
||||
---
|
||||
|
||||
## Web-Interface
|
||||
|
||||
### Kopierstatus
|
||||
|
||||
Zeigt den Live-Fortschritt mit:
|
||||
- Prozentualer Fortschritt + Fortschrittsbalken
|
||||
- Dateizähler (`23 / 147 Dateien`)
|
||||
- Übertragene Datenmenge (`1.2 GB / 3.5 GB`)
|
||||
- Geschwindigkeit (`12.4 MB/s`)
|
||||
- Verbleibende Zeit (`⏱ noch ca. 4 Min.`)
|
||||
- Aktuelle Datei
|
||||
- Phasen-Anzeige: *Kopieren → Verifizieren → Quelle leeren*
|
||||
|
||||
Nach dem Abschluss: Zusammenfassung mit ✕-Button (verschwindet nach 5 Minuten automatisch).
|
||||
|
||||
### USB Port Konfiguration & Datei-Explorer
|
||||
|
||||
```
|
||||
┌─────────────────┬─────────────────┬──────────────────────┐
|
||||
│ ▲ QUELLE │ ▼ ZIEL │ ⬆ Quelle ⬇ Ziel ↻ │
|
||||
│ ● Port 2-2 │ ○ Port 1-1 │ ──────────────────── │
|
||||
│ Samsung USB │ Nicht verbunden│ 📁 DCIM │
|
||||
│ │ │ 📁 MISC │
|
||||
│ [Als Quelle ▾] │ [Als Ziel ▾] │ 🖼 IMG_001.jpg 4 MB │
|
||||
└─────────────────┴─────────────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
- **Grüner Punkt** = Gerät verbunden und bereit
|
||||
- **Grauer Punkt** = Port konfiguriert, kein Gerät eingesteckt
|
||||
- **Datei-Explorer** zum Durchsuchen der verbundenen Laufwerke
|
||||
|
||||
### Kopier-Einstellungen
|
||||
|
||||
| Einstellung | Standard | Beschreibung |
|
||||
|---|---|---|
|
||||
| Datumsformat | `JJJJ-MM-TT` | Format des Zielordners |
|
||||
| Uhrzeit | ✓ | Uhrzeit im Ordnernamen (`_143022`) |
|
||||
| Unterordner | ✓ | Unterordner nach Gerätebezeichnung |
|
||||
| Auto-Kopie | ✓ | Automatisch starten wenn beide verbunden |
|
||||
| Dateifilter | *leer* | Nur bestimmte Dateitypen kopieren |
|
||||
| Systemdateien | ✓ | `.DS_Store`, `Thumbs.db`, `RECYCLER` usw. ausschließen |
|
||||
| Duplikate | Überspringen | Skip / Überschreiben / Umbenennen |
|
||||
| MD5-Verify | ✗ | Jede Datei nach dem Kopieren prüfen |
|
||||
| Quelle leeren | ✗ | Quelldateien nach Kopieren löschen |
|
||||
|
||||
#### Dateifilter – Schnell-Presets
|
||||
|
||||
| Preset | Dateitypen |
|
||||
|---|---|
|
||||
| 📷 Fotos | jpg, jpeg, heic, raw, cr2, nef, arw, dng, png |
|
||||
| 🎬 Videos | mp4, mov, avi, mkv, mts, m2ts, wmv |
|
||||
| 📷+🎬 Beides | Fotos + Videos kombiniert |
|
||||
| ✕ Alle | Kein Filter – alle Dateien kopieren |
|
||||
|
||||
### Fernkopie – NAS / SMB
|
||||
|
||||
Nach dem lokalen Kopieren lädt PiCopy auf konfigurierte NAS-Freigaben hoch:
|
||||
|
||||
1. *+ NAS-Ziel hinzufügen* klicken
|
||||
2. Name, Server-IP, Freigabename, Benutzer und Passwort eingeben
|
||||
3. *Speichern & Verbindung testen* – PiCopy testet die Verbindung sofort
|
||||
4. Mehrere NAS-Ziele möglich, jedes einzeln aktivierbar
|
||||
|
||||
### WiFi-Einstellungen
|
||||
|
||||
| Modus | Beschreibung |
|
||||
|---|---|
|
||||
| **Heimnetz** | WLAN-Name und Passwort für die Router-Verbindung |
|
||||
| **Hotspot (AP)** | Eigenes WLAN wenn kein Heimnetz erreichbar |
|
||||
|
||||
**Hotspot-Standardwerte:**
|
||||
- SSID: `PiCopy`
|
||||
- Passwort: `PiCopy,`
|
||||
- IP im Hotspot-Modus: `http://10.42.0.1:8080`
|
||||
|
||||
Der Hotspot startet automatisch beim Boot wenn das konfigurierte WLAN nicht verfügbar ist.
|
||||
|
||||
---
|
||||
|
||||
## Ordnerstruktur auf dem Ziel
|
||||
|
||||
```
|
||||
/ziel-laufwerk/
|
||||
└── 2024-01-15_143022/ ← Datum + Uhrzeit (konfigurierbar)
|
||||
└── Samsung_USB/ ← Gerätebezeichnung (wenn Unterordner aktiv)
|
||||
├── DCIM/
|
||||
│ └── 100CANON/
|
||||
│ ├── IMG_0001.JPG
|
||||
│ ├── IMG_0001.CR2
|
||||
│ └── IMG_0002.MP4
|
||||
└── MISC/
|
||||
└── notes.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update
|
||||
|
||||
### Automatische Update-Benachrichtigung
|
||||
|
||||
PiCopy prüft alle **6 Stunden** automatisch ob eine neue Version verfügbar ist. Sobald ein Update bereitsteht, erscheint in der Topbar des Web-Interfaces ein gelbes Badge:
|
||||
|
||||
```
|
||||
↑ v1.1.0 verfügbar
|
||||
```
|
||||
|
||||
Ein Klick auf das Badge → Bestätigungsdialog → PiCopy lädt die neue Version herunter, verifiziert sie und startet sich selbst neu. Das Interface ist dabei ca. 10 Sekunden nicht erreichbar.
|
||||
|
||||
### Manuelles Update
|
||||
|
||||
**Option A – über das Web-Interface:**
|
||||
Topbar-Badge klicken (falls Update verfügbar) oder direkt:
|
||||
`http://<pi-ip>:8080` → Badge erscheint automatisch
|
||||
|
||||
**Option B – per SSH:**
|
||||
|
||||
```bash
|
||||
cd PiCopy
|
||||
git pull
|
||||
sudo cp app.py /opt/picopy/app.py
|
||||
sudo systemctl restart picopy
|
||||
```
|
||||
|
||||
**Option C – One-Liner:**
|
||||
|
||||
```bash
|
||||
curl -sSL https://git.leuschner.dev/Tobias/PiCopy/raw/branch/main/app.py \
|
||||
| sudo tee /opt/picopy/app.py > /dev/null && sudo systemctl restart picopy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deinstallation
|
||||
|
||||
```bash
|
||||
sudo systemctl stop picopy
|
||||
sudo systemctl disable picopy
|
||||
sudo rm /etc/systemd/system/picopy.service
|
||||
sudo rm -rf /opt/picopy
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service-Verwaltung
|
||||
|
||||
```bash
|
||||
# Status prüfen
|
||||
sudo systemctl status picopy
|
||||
|
||||
# Live-Logs
|
||||
journalctl -u picopy -f
|
||||
|
||||
# Neustart
|
||||
sudo systemctl restart picopy
|
||||
|
||||
# Stoppen
|
||||
sudo systemctl stop picopy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technische Details
|
||||
|
||||
| Komponente | Technologie |
|
||||
|---|---|
|
||||
| Backend | Python 3 + Flask |
|
||||
| USB-Erkennung | `lsblk` + `udevadm` |
|
||||
| USB-Monitoring | `pyudev` (udev-Events) |
|
||||
| WiFi-Verwaltung | NetworkManager (`nmcli`) |
|
||||
| NAS-Sync | `rclone` (SMB) |
|
||||
| Service | systemd (Autostart, Auto-Restart) |
|
||||
|
||||
**Dateipfade auf dem Pi:**
|
||||
|
||||
| Pfad | Inhalt |
|
||||
|---|---|
|
||||
| `/opt/picopy/app.py` | Hauptanwendung |
|
||||
| `/opt/picopy/config.json` | Konfiguration (Ports, WiFi, Einstellungen) |
|
||||
| `/opt/picopy/state.json` | Letzter Kopierstatus (persisted) |
|
||||
| `/opt/picopy/rclone.conf` | NAS-Zugangsdaten (rclone) |
|
||||
| `/opt/picopy/logs/picopy.log` | Log-Datei |
|
||||
| `/etc/systemd/system/picopy.service` | Systemd-Service |
|
||||
|
||||
---
|
||||
|
||||
## Getestete Hardware
|
||||
|
||||
| Gerät | Status |
|
||||
|---|---|
|
||||
| Raspberry Pi 4 Model B | ✅ Vollständig getestet |
|
||||
| Raspberry Pi 5 | ✅ Kompatibel |
|
||||
| Raspberry Pi 3 Model B+ | ✅ Kompatibel |
|
||||
| Raspberry Pi Zero 2W | ⚠️ Langsamer, nur 1 USB-Port (Hub benötigt) |
|
||||
|
||||
---
|
||||
|
||||
## Neue Version veröffentlichen (für Maintainer)
|
||||
|
||||
So wird ein neues Release erstellt, das alle Nutzer automatisch als Update angezeigt bekommen:
|
||||
|
||||
**1. Versionen erhöhen**
|
||||
|
||||
In `app.py`:
|
||||
```python
|
||||
VERSION = '1.1.0' # ← neue Versionsnummer
|
||||
```
|
||||
|
||||
In `version.txt`:
|
||||
```
|
||||
1.1.0
|
||||
```
|
||||
|
||||
**2. Committen & pushen**
|
||||
|
||||
```bash
|
||||
git add app.py version.txt
|
||||
git commit -m "Release v1.1.0"
|
||||
git push
|
||||
```
|
||||
|
||||
**3. Release/Tag in Gitea erstellen** *(optional, aber empfohlen)*
|
||||
|
||||
Unter [git.leuschner.dev/Tobias/PiCopy/releases](https://git.leuschner.dev/Tobias/PiCopy/releases) → *Neues Release* → Tag `v1.1.0` setzen.
|
||||
|
||||
**Das war's.** Alle laufenden PiCopy-Instanzen erkennen das Update innerhalb von 6 Stunden automatisch und zeigen das Badge im Web-Interface an.
|
||||
|
||||
> **Hinweis:** `version.txt` und `app.py` müssen immer dieselbe Versionsnummer haben. Die `version.txt` ist der einzige Vergleichspunkt – der Inhalt der `app.py` wird erst beim tatsächlichen Update-Install heruntergeladen.
|
||||
|
||||
---
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT License – siehe [LICENSE](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
## Autor
|
||||
|
||||
Tobias Leuschner – [info@leuschner.dev](mailto:info@leuschner.dev)
|
||||
149
app.py
149
app.py
@@ -10,12 +10,17 @@ import threading
|
||||
import subprocess
|
||||
import time
|
||||
import uuid as _uuid_mod
|
||||
import urllib.request as _urlreq
|
||||
import urllib.error as _urlerr
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
VERSION = '1.0.1'
|
||||
RAW_BASE = 'https://git.leuschner.dev/Tobias/PiCopy/raw/branch/main'
|
||||
|
||||
BASE_DIR = Path('/opt/picopy')
|
||||
CONFIG_FILE = BASE_DIR / 'config.json'
|
||||
STATE_FILE = BASE_DIR / 'state.json'
|
||||
@@ -953,6 +958,96 @@ def r_browse():
|
||||
|
||||
|
||||
|
||||
# ── Update-System ─────────────────────────────────────────────────────────────
|
||||
|
||||
update_state = {
|
||||
'current': VERSION,
|
||||
'latest': None,
|
||||
'available': False,
|
||||
'checking': False,
|
||||
'error': None,
|
||||
'last_checked': None,
|
||||
}
|
||||
update_lock = threading.Lock()
|
||||
|
||||
|
||||
def _vtuple(v):
|
||||
try:
|
||||
return tuple(int(x) for x in v.strip().lstrip('v').split('.'))
|
||||
except Exception:
|
||||
return (0,)
|
||||
|
||||
|
||||
def check_for_updates():
|
||||
with update_lock:
|
||||
if update_state['checking']:
|
||||
return
|
||||
update_state['checking'] = True
|
||||
update_state['error'] = None
|
||||
|
||||
try:
|
||||
req = _urlreq.urlopen(f'{RAW_BASE}/version.txt', timeout=10)
|
||||
latest = req.read().decode().strip()
|
||||
avail = _vtuple(latest) > _vtuple(VERSION)
|
||||
with update_lock:
|
||||
update_state.update(latest=latest, available=avail,
|
||||
last_checked=datetime.now().isoformat())
|
||||
if avail:
|
||||
log.info(f'Update verfügbar: {VERSION} → {latest}')
|
||||
except Exception as e:
|
||||
with update_lock:
|
||||
update_state['error'] = str(e)
|
||||
log.warning(f'Update-Check fehlgeschlagen: {e}')
|
||||
finally:
|
||||
with update_lock:
|
||||
update_state['checking'] = False
|
||||
|
||||
|
||||
def update_check_loop():
|
||||
time.sleep(30) # Kurz nach Start einmalig prüfen
|
||||
while True:
|
||||
check_for_updates()
|
||||
time.sleep(6 * 3600) # Dann alle 6 Stunden
|
||||
|
||||
|
||||
@app.route('/api/update/status')
|
||||
def r_update_status():
|
||||
with update_lock:
|
||||
return jsonify(dict(update_state))
|
||||
|
||||
|
||||
@app.route('/api/update/check', methods=['POST'])
|
||||
def r_update_check():
|
||||
threading.Thread(target=check_for_updates, daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@app.route('/api/update/install', methods=['POST'])
|
||||
def r_update_install():
|
||||
try:
|
||||
log.info('Update wird heruntergeladen…')
|
||||
req = _urlreq.urlopen(f'{RAW_BASE}/app.py', timeout=60)
|
||||
new_code = req.read().decode()
|
||||
|
||||
# Syntax-Check bevor wir irgendetwas überschreiben
|
||||
compile(new_code, 'app.py', 'exec')
|
||||
|
||||
tmp = Path('/tmp/picopy_update.py')
|
||||
tmp.write_text(new_code)
|
||||
shutil.copy(str(tmp), '/opt/picopy/app.py')
|
||||
log.info('Update installiert – starte Dienst neu…')
|
||||
|
||||
# Systemd startet den Dienst automatisch neu
|
||||
subprocess.Popen(['systemctl', 'restart', 'picopy'])
|
||||
return jsonify(ok=True)
|
||||
|
||||
except SyntaxError as e:
|
||||
return jsonify(error=f'Update-Datei ungültig: {e}'), 500
|
||||
except Exception as e:
|
||||
log.exception('Update fehlgeschlagen')
|
||||
return jsonify(error=str(e)), 500
|
||||
|
||||
|
||||
# ── HTML Template ─────────────────────────────────────────────────────────────
|
||||
|
||||
HTML = r"""<!DOCTYPE html>
|
||||
@@ -994,6 +1089,8 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
.wdot.c{background:var(--grn);box-shadow:0 0 6px var(--grn)}
|
||||
.wdot.a{background:var(--pur)}
|
||||
.wdot.d{background:var(--brd2)}
|
||||
.upd-badge{display:none;align-items:center;gap:.4rem;font-size:.78rem;font-weight:600;background:rgba(251,191,36,.12);border:1px solid rgba(251,191,36,.4);color:var(--ylw);border-radius:9999px;padding:.28rem .75rem;cursor:pointer;transition:.15s;white-space:nowrap}
|
||||
.upd-badge:hover{background:rgba(251,191,36,.22)}
|
||||
#wifi-label{font-weight:600;color:var(--txt)}
|
||||
#wifi-ip{color:var(--sub);font-family:monospace;font-size:.76rem}
|
||||
|
||||
@@ -1110,7 +1207,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
.expl-empty{padding:1.5rem;text-align:center;color:var(--sub);font-size:.84rem}
|
||||
|
||||
/* ── Log ── */
|
||||
.log-wrap{font-family:ui-monospace,monospace;font-size:.75rem;max-height:200px;overflow-y:auto;background:var(--bg2);border-radius:.45rem;padding:.5rem}
|
||||
.log-wrap{font-family:ui-monospace,monospace;font-size:.75rem;max-height:300px;overflow-y:auto;background:var(--bg2);border-radius:.45rem;padding:.5rem}
|
||||
.log-row{display:flex;gap:.5rem;padding:.18rem 0;border-bottom:1px solid rgba(42,54,80,.5)}
|
||||
.log-row:last-child{border-bottom:none}
|
||||
.log-t{color:var(--brd2);flex-shrink:0}
|
||||
@@ -1423,8 +1520,8 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Protokoll (neben WiFi) ── -->
|
||||
<div class="card">
|
||||
<!-- ── Protokoll ── -->
|
||||
<div class="card col2">
|
||||
<div class="card-head">
|
||||
<div class="card-icon" style="background:rgba(139,154,181,.1);color:var(--sub)">≡</div>
|
||||
<span class="card-title">Protokoll</span>
|
||||
@@ -1832,6 +1929,47 @@ function dismissStatus(){
|
||||
$('st-dismiss').style.display='none';
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────────
|
||||
async function pollUpdate() {
|
||||
try {
|
||||
const u = await api('/update/status');
|
||||
const badge = $('upd-badge'), vEl = $('upd-version');
|
||||
if (u.available && u.latest) {
|
||||
vEl.textContent = 'v' + u.latest;
|
||||
badge.style.display = 'flex';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
const u = await api('/update/status');
|
||||
const latest = (u.latest || '?');
|
||||
if (!confirm(
|
||||
'Update auf v' + latest + ' installieren?\n\n' +
|
||||
'PiCopy lädt die neue Version herunter und startet neu.\n' +
|
||||
'Das Web-Interface ist für ca. 10 Sekunden nicht erreichbar.'
|
||||
)) return;
|
||||
|
||||
$('upd-badge').innerHTML = '↻ Installiere…';
|
||||
$('upd-badge').style.pointerEvents = 'none';
|
||||
|
||||
try {
|
||||
await api('/update/install', 'POST');
|
||||
} catch(e) {}
|
||||
|
||||
// Warte bis der Dienst wieder läuft, dann reload
|
||||
setTimeout(async function waitForRestart() {
|
||||
try {
|
||||
await fetch('/api/update/status');
|
||||
location.reload();
|
||||
} catch(e) {
|
||||
setTimeout(waitForRestart, 2000);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function flash(id,cls,msg){
|
||||
const el=$(id); el.className='flash '+cls; el.textContent=msg; el.style.display='block';
|
||||
if(cls==='ok') setTimeout(()=>el.style.display='none',3500);
|
||||
@@ -1844,7 +1982,9 @@ function flash(id,cls,msg){
|
||||
expl.load('');
|
||||
setInterval(poll,1500);
|
||||
setInterval(refreshDevices,8000);
|
||||
setInterval(pollUpdate,60000);
|
||||
poll();
|
||||
pollUpdate();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
@@ -1855,5 +1995,6 @@ if __name__ == '__main__':
|
||||
load_state()
|
||||
threading.Thread(target=usb_monitor, daemon=True).start()
|
||||
threading.Thread(target=wifi_monitor, daemon=True).start()
|
||||
log.info('PiCopy v2 läuft auf http://0.0.0.0:8080')
|
||||
threading.Thread(target=update_check_loop, daemon=True).start()
|
||||
log.info(f'PiCopy v{VERSION} läuft auf http://0.0.0.0:8080')
|
||||
app.run(host='0.0.0.0', port=8080, debug=False, use_reloader=False)
|
||||
|
||||
21
deploy.sh
21
deploy.sh
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
# deploy.sh - Überträgt PiCopy zum Pi und installiert es
|
||||
# Verwendung: bash deploy.sh
|
||||
# Benötigt: sshpass (brew install sshpass)
|
||||
|
||||
PI_HOST="10.0.100.61"
|
||||
PI_USER="tobias"
|
||||
PI_PASS="dmu7uqMH9roYzdtovlm0XfXT6"
|
||||
REMOTE="/home/tobias/picopy_deploy"
|
||||
|
||||
SSH="sshpass -p '$PI_PASS' ssh -o StrictHostKeyChecking=no $PI_USER@$PI_HOST"
|
||||
SCP="sshpass -p '$PI_PASS' scp -o StrictHostKeyChecking=no"
|
||||
|
||||
echo ">> Dateien übertragen..."
|
||||
eval "$SCP -r $(pwd)/. $PI_USER@$PI_HOST:$REMOTE/"
|
||||
|
||||
echo ">> Installation starten..."
|
||||
eval "$SSH 'cd $REMOTE && sudo bash install.sh'"
|
||||
|
||||
echo ">> Fertig!"
|
||||
eval "$SSH 'sudo systemctl status picopy --no-pager'"
|
||||
141
install.sh
141
install.sh
@@ -1,42 +1,111 @@
|
||||
#!/bin/bash
|
||||
# PiCopy Installations-Script für den Raspberry Pi
|
||||
# Ausführen auf dem Pi als root oder mit sudo:
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PiCopy – Installer
|
||||
# https://git.leuschner.dev/Tobias/PiCopy
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash install.sh
|
||||
# or one-line:
|
||||
# curl -sSL https://git.leuschner.dev/Tobias/PiCopy/raw/branch/main/install.sh | sudo bash
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
set -e
|
||||
INSTALL_DIR="/opt/picopy"
|
||||
SERVICE_NAME="picopy"
|
||||
PORT=8080
|
||||
REPO_RAW="https://git.leuschner.dev/Tobias/PiCopy/raw/branch/main"
|
||||
|
||||
PI_DIR="/opt/picopy"
|
||||
SERVICE="picopy"
|
||||
# ── Farben ───────────────────────────────────────────────────────────────────
|
||||
R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m'; N='\033[0m'
|
||||
info() { echo -e "${B}[PiCopy]${N} $1"; }
|
||||
ok() { echo -e "${G}[ OK ]${N} $1"; }
|
||||
warn() { echo -e "${Y}[ WARN ]${N} $1"; }
|
||||
fail() { echo -e "${R}[ FAIL ]${N} $1"; exit 1; }
|
||||
|
||||
echo "=== PiCopy Installation ==="
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
echo ">> Pakete installieren..."
|
||||
apt-get update -q
|
||||
apt-get install -y python3 python3-venv python3-pip lsblk
|
||||
|
||||
# Verzeichnis anlegen
|
||||
echo ">> Verzeichnis anlegen: $PI_DIR"
|
||||
mkdir -p "$PI_DIR/logs"
|
||||
|
||||
# Python-Umgebung
|
||||
echo ">> Python venv erstellen..."
|
||||
python3 -m venv "$PI_DIR/venv"
|
||||
"$PI_DIR/venv/bin/pip" install --quiet flask pyudev
|
||||
|
||||
# App-Dateien kopieren
|
||||
echo ">> Dateien kopieren..."
|
||||
cp app.py "$PI_DIR/app.py"
|
||||
|
||||
# Systemd-Service einrichten
|
||||
echo ">> Systemd-Service einrichten..."
|
||||
cp picopy.service "/etc/systemd/system/$SERVICE.service"
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE"
|
||||
systemctl restart "$SERVICE"
|
||||
# ── Voraussetzungen ───────────────────────────────────────────────────────────
|
||||
[ "$EUID" -eq 0 ] || fail "Bitte als root ausführen: sudo bash install.sh"
|
||||
command -v apt-get &>/dev/null || fail "apt-get nicht gefunden (nur Debian/Raspberry Pi OS unterstützt)"
|
||||
|
||||
echo ""
|
||||
echo "=== Installation abgeschlossen ==="
|
||||
echo "Web-Interface: http://$(hostname -I | awk '{print $1}'):8080"
|
||||
echo "Status: systemctl status $SERVICE"
|
||||
echo "Logs: journalctl -u $SERVICE -f"
|
||||
echo -e "${B}╔══════════════════════════════════════════╗${N}"
|
||||
echo -e "${B}║ PiCopy – Installation ║${N}"
|
||||
echo -e "${B}╚══════════════════════════════════════════╝${N}"
|
||||
echo ""
|
||||
|
||||
# ── System-Pakete ─────────────────────────────────────────────────────────────
|
||||
info "Systemabhängigkeiten werden installiert..."
|
||||
apt-get update -q
|
||||
apt-get install -y -q python3 python3-venv python3-pip util-linux rclone
|
||||
ok "Systemabhängigkeiten installiert"
|
||||
|
||||
# ── Verzeichnis anlegen ───────────────────────────────────────────────────────
|
||||
info "Installationsverzeichnis: $INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR/logs"
|
||||
|
||||
# ── App-Datei kopieren oder herunterladen ─────────────────────────────────────
|
||||
if [ -f "./app.py" ]; then
|
||||
info "Lokale app.py wird verwendet..."
|
||||
cp app.py "$INSTALL_DIR/app.py"
|
||||
else
|
||||
info "app.py wird heruntergeladen..."
|
||||
curl -sSfL "$REPO_RAW/app.py" -o "$INSTALL_DIR/app.py" \
|
||||
|| fail "Download fehlgeschlagen. Prüfe die Internet-Verbindung."
|
||||
fi
|
||||
ok "app.py installiert"
|
||||
|
||||
# ── Python-Umgebung ───────────────────────────────────────────────────────────
|
||||
info "Python venv wird erstellt..."
|
||||
python3 -m venv "$INSTALL_DIR/venv"
|
||||
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade pip
|
||||
"$INSTALL_DIR/venv/bin/pip" install --quiet flask pyudev
|
||||
ok "Python-Umgebung erstellt"
|
||||
|
||||
# ── Systemd-Service ───────────────────────────────────────────────────────────
|
||||
info "Systemd-Service wird eingerichtet..."
|
||||
|
||||
if [ -f "./picopy.service" ]; then
|
||||
cp picopy.service /etc/systemd/system/picopy.service
|
||||
else
|
||||
cat > /etc/systemd/system/picopy.service << 'EOF'
|
||||
[Unit]
|
||||
Description=PiCopy – Automatischer USB-Kopierdienst
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/picopy
|
||||
ExecStart=/opt/picopy/venv/bin/python /opt/picopy/app.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
systemctl restart "$SERVICE_NAME"
|
||||
|
||||
# ── Ergebnis ─────────────────────────────────────────────────────────────────
|
||||
sleep 3
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
IP=$(hostname -I | awk '{print $1}')
|
||||
echo ""
|
||||
echo -e "${G}╔══════════════════════════════════════════╗${N}"
|
||||
echo -e "${G}║ PiCopy ist bereit! ║${N}"
|
||||
echo -e "${G}╚══════════════════════════════════════════╝${N}"
|
||||
echo ""
|
||||
echo -e " Web-Interface: ${B}http://$IP:$PORT${N}"
|
||||
echo ""
|
||||
echo " Nützliche Befehle:"
|
||||
echo " sudo systemctl status $SERVICE_NAME # Status"
|
||||
echo " journalctl -u $SERVICE_NAME -f # Live-Logs"
|
||||
echo " sudo systemctl restart $SERVICE_NAME # Neustart"
|
||||
echo ""
|
||||
else
|
||||
fail "PiCopy konnte nicht gestartet werden."$'\n'"Logs: journalctl -u $SERVICE_NAME -n 50"
|
||||
fi
|
||||
|
||||
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
||||
1.0.1
|
||||
Reference in New Issue
Block a user