🗑️ WebDAV komplett entfernt
✨ Bereinigungen: - Alle WebDAV-bezogenen Code-Teile entfernt - Dokumentation auf HTTP-Zugriff umgestellt - Veraltete WebDAV-Backup-Dateien gelöscht - URLs von /webdav/* auf direkte Pfade geändert 📁 Gelöschte Dateien: - WEBDAV-INTEGRATION.md - SCHREIBSCHUTZ-BEHOBEN.md - server_old.js - server_webdav_backup.js 🔄 Aktualisierte URLs: - /webdav/templates/ → /templates/ - /webdav/documents/ → /documents/ 📝 Bereinigte Dokumentation: - README.md - WebDAV-Referenzen entfernt - STATUS.md - URLs aktualisiert - SSL-*.md - WebDAV-Links ersetzt - Start-Scripts - Pfade korrigiert 🎯 Fokus jetzt auf: - HTTP-basierte Dateifreigabe - Management-GUI System - Custom Tags REST-APIs - Template-Verarbeitung ohne WebDAV-Komplexität
This commit is contained in:
parent
88932bfbf4
commit
83d345075b
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,7 +4,6 @@ uploads/
|
||||
.env
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
webdav-tree.json
|
||||
203_cert.pem
|
||||
203_key.pem
|
||||
private-key.pem
|
||||
|
||||
49
README.md
49
README.md
@ -1,6 +1,6 @@
|
||||
# DOCX Template Server
|
||||
|
||||
Ein Node.js-Server für die automatische Verarbeitung von DOCX-Templates mit WebDAV-ähnlicher Dateifreigabe.
|
||||
Ein Node.js-Server für die automatische Verarbeitung von DOCX-Templates mit Management-GUI.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@ -25,9 +25,11 @@ npm start
|
||||
|
||||
## 🌐 Zugriff
|
||||
|
||||
- **Web-Interface:** http://localhost:80
|
||||
- **Templates:** http://localhost:80/webdav/templates/
|
||||
- **Dokumente:** http://localhost:80/webdav/documents/
|
||||
### 🌐 **Web-Zugriff:**
|
||||
- **Server:** http://localhost:80/
|
||||
- **Management:** http://localhost:3000/
|
||||
- **Templates:** http://localhost:80/templates/
|
||||
- **Dokumente:** http://localhost:80/documents/
|
||||
|
||||
## 📋 Verwendung
|
||||
|
||||
@ -41,7 +43,8 @@ Erstellen Sie ein DOCX-Dokument mit Tags wie:
|
||||
### 2. Template hochladen
|
||||
- Über Web-Interface: Datei auswählen und hochladen
|
||||
- Über API: `POST /upload-template`
|
||||
- Über Dateifreigabe: Datei in `/webdav/templates/` kopieren
|
||||
- Über Web-Upload: http://localhost:80/ → "Template hochladen"
|
||||
- Direkt in Ordner: Datei in `/templates/` kopieren
|
||||
|
||||
### 3. Automatische Verarbeitung
|
||||
Der Server erkennt automatisch alle Tags und füllt sie mit passenden Demo-Daten:
|
||||
@ -125,25 +128,12 @@ Body: {
|
||||
}
|
||||
```
|
||||
|
||||
## 💾 Dateifreigabe Setup
|
||||
## 🌐 HTTP-Zugriff
|
||||
|
||||
### Windows Explorer
|
||||
1. Windows Explorer öffnen
|
||||
2. Adressleiste: `\\localhost\webdav$`
|
||||
3. Oder: "Netzlaufwerk verbinden" → `http://localhost:80/webdav/templates/`
|
||||
|
||||
### Linux/macOS
|
||||
```bash
|
||||
# Verzeichnis erstellen
|
||||
sudo mkdir -p /mnt/docx-templates
|
||||
sudo mkdir -p /mnt/docx-documents
|
||||
|
||||
# Mounten (davfs2 erforderlich)
|
||||
sudo mount -t davfs http://localhost:80/webdav/templates/ /mnt/docx-templates
|
||||
sudo mount -t davfs http://localhost:80/webdav/documents/ /mnt/docx-documents
|
||||
|
||||
# Oder über GUI: Dateimanager → "Mit Server verbinden" → http://localhost:80/webdav/templates/
|
||||
```
|
||||
### Direkte HTTP-URLs:
|
||||
Templates und generierte Dokumente sind über HTTP zugänglich:
|
||||
- **Templates:** http://localhost:80/templates/
|
||||
- **Dokumente:** http://localhost:80/documents/
|
||||
|
||||
## 🔒 SSL/HTTPS Einrichtung
|
||||
|
||||
@ -297,15 +287,10 @@ ls -la private-key.pem certificate.pem
|
||||
- Tags richtig geschrieben (`{tag}` nicht `{{tag}}`)?
|
||||
- Datei-Berechtigungen prüfen
|
||||
|
||||
### Dateifreigabe funktioniert nicht
|
||||
```bash
|
||||
# WebDAV-Client installieren
|
||||
# Ubuntu/Debian:
|
||||
sudo apt install davfs2
|
||||
|
||||
# macOS:
|
||||
brew install davfs2
|
||||
```
|
||||
### Templates nicht sichtbar
|
||||
Prüfen Sie:
|
||||
- Server läuft: `http://localhost:80/`
|
||||
- Templates-Ordner: `http://localhost:80/templates/`
|
||||
|
||||
### SSL-Probleme
|
||||
```bash
|
||||
|
||||
@ -1,127 +0,0 @@
|
||||
# 🔓 SCHREIBSCHUTZ-PROBLEM BEHOBEN!
|
||||
|
||||
## ✅ **Erweiterte WebDAV-Implementierung**
|
||||
|
||||
Ich habe die WebDAV-Implementierung erweitert, um das Schreibschutz-Problem zu lösen:
|
||||
|
||||
### 🔧 **Was wurde verbessert:**
|
||||
|
||||
#### **1. Erweiterte WebDAV-Properties:**
|
||||
- `<D:isreadonly>F</D:isreadonly>` - Explizit NICHT schreibgeschützt
|
||||
- `<O:IsReadonly>F</O:IsReadonly>` - Microsoft Office spezifisch
|
||||
- `<D:executable>F</D:executable>` - Nicht ausführbar
|
||||
- `<D:ishidden>F</D:ishidden>` - Nicht versteckt
|
||||
|
||||
#### **2. Office-kompatible HTTP-Header:**
|
||||
- `Server: Microsoft-IIS/10.0` - Word erkennt als IIS-Server
|
||||
- `DAV: 1, 2, ordered-collections, versioning` - Vollständige WebDAV-Unterstützung
|
||||
- `Cache-Control: no-cache` - Keine Zwischenspeicherung
|
||||
- `ETag` und `Last-Modified` für Versionskontrolle
|
||||
|
||||
#### **3. Automatische Dateiberechtigungen:**
|
||||
- Dateien werden mit `chmod 666` (rw-rw-rw-) gespeichert
|
||||
- Explizite Schreibrechte für alle Benutzer
|
||||
|
||||
#### **4. Bessere Lock-Unterstützung:**
|
||||
- Sowohl exklusive als auch geteilte Locks
|
||||
- Office-kompatible Lock-Token
|
||||
|
||||
### 📝 **Neue Word-Integration:**
|
||||
|
||||
#### **Schritt 1: WebDAV-Netzlaufwerk einrichten**
|
||||
```
|
||||
1. Windows Explorer öffnen
|
||||
2. "Dieser PC" → Rechtsklick → "Netzlaufwerk verbinden"
|
||||
3. Ordner: \\localhost\webdav\templates (oder \documents)
|
||||
4. Port: 80
|
||||
5. NICHT "Mit anderen Anmeldeinformationen" ankreuzen
|
||||
```
|
||||
|
||||
#### **Schritt 2: In Word öffnen**
|
||||
```
|
||||
1. Word öffnen
|
||||
2. Datei → Öffnen
|
||||
3. Das neue Netzlaufwerk auswählen
|
||||
4. Template öffnen
|
||||
```
|
||||
|
||||
#### **Schritt 3: Test der Schreibrechte**
|
||||
```
|
||||
1. Text bearbeiten
|
||||
2. Strg+S drücken
|
||||
3. Sollte jetzt DIREKT auf dem Server speichern!
|
||||
```
|
||||
|
||||
### 🚨 **Troubleshooting:**
|
||||
|
||||
#### **Falls immer noch schreibgeschützt:**
|
||||
|
||||
**Option 1: Dateiberechtigungen prüfen**
|
||||
```bash
|
||||
cd /home/OfficeServerJS/templates
|
||||
ls -la *.docx
|
||||
# Sollte zeigen: -rw-rw-rw-
|
||||
```
|
||||
|
||||
**Option 2: Alternative WebDAV-URL**
|
||||
```
|
||||
Statt: http://localhost:80/webdav/templates/
|
||||
Verwenden: http://localhost/webdav/templates/
|
||||
Oder: http://127.0.0.1:80/webdav/templates/
|
||||
```
|
||||
|
||||
**Option 3: Windows WebDAV-Client konfigurieren**
|
||||
```cmd
|
||||
# Als Administrator ausführen:
|
||||
net stop webclient
|
||||
net start webclient
|
||||
|
||||
# WebDAV Registry-Einstellungen:
|
||||
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters /v BasicAuthLevel /t REG_DWORD /d 2 /f
|
||||
```
|
||||
|
||||
**Option 4: Office Trust Center**
|
||||
```
|
||||
1. Word → Datei → Optionen
|
||||
2. Trust Center → Trust Center-Einstellungen
|
||||
3. Geschützte Ansicht → Alle Häkchen entfernen
|
||||
4. Datenschutz-Optionen → "Dateien aus dem Internet..." deaktivieren
|
||||
```
|
||||
|
||||
### 🔍 **Live-Monitoring:**
|
||||
|
||||
Der Server zeigt jetzt Live-Logs:
|
||||
```
|
||||
📖 Word öffnet Template: rechnung_template.docx
|
||||
📝 Word speichert Template: rechnung_template.docx
|
||||
✅ Template gespeichert: rechnung_template.docx
|
||||
```
|
||||
|
||||
### 🎯 **Test-Verfahren:**
|
||||
|
||||
1. **Server-Logs beobachten** beim Öffnen/Speichern
|
||||
2. **Datei-Timestamps prüfen** nach dem Speichern
|
||||
3. **Browser-Zugriff testen**: http://localhost:80/webdav/templates/
|
||||
|
||||
### ⚡ **Sofort-Test:**
|
||||
|
||||
```bash
|
||||
# 1. Datei über WebDAV hochladen
|
||||
curl -X PUT --data-binary @test.docx http://localhost:80/webdav/templates/test.docx
|
||||
|
||||
# 2. Berechtigung prüfen
|
||||
ls -la /home/OfficeServerJS/templates/test.docx
|
||||
|
||||
# 3. WebDAV-Properties abfragen
|
||||
curl -X PROPFIND -H "Depth: 1" http://localhost:80/webdav/templates/test.docx
|
||||
```
|
||||
|
||||
## 🎉 **Ergebnis:**
|
||||
|
||||
✅ **Erweiterte WebDAV-Properties** für Office-Kompatibilität
|
||||
✅ **Explizite Schreibrechte** in XML-Response
|
||||
✅ **Automatische Dateiberechtigungen** beim Speichern
|
||||
✅ **Office-optimierte HTTP-Header**
|
||||
✅ **Live-Monitoring** für Debugging
|
||||
|
||||
**Word sollte jetzt die Dateien als beschreibbar erkennen und direkt auf dem Server speichern! 🔓✍️**
|
||||
@ -41,13 +41,13 @@ cd /home/OfficeServerJS
|
||||
|
||||
### HTTP (immer verfügbar):
|
||||
- **Web-Interface:** http://localhost:80
|
||||
- **Templates:** http://localhost:80/webdav/templates/
|
||||
- **Dokumente:** http://localhost:80/webdav/documents/
|
||||
- **Templates:** http://localhost:80/templates/
|
||||
- **Dokumente:** http://localhost:80/documents/
|
||||
|
||||
### HTTPS (nach SSL-Aktivierung):
|
||||
- **Web-Interface:** https://localhost:443
|
||||
- **Templates:** https://localhost:443/webdav/templates/
|
||||
- **Dokumente:** https://localhost:443/webdav/documents/
|
||||
- **Templates:** https://localhost:443/templates/
|
||||
- **Dokumente:** https://localhost:443/documents/
|
||||
|
||||
## 🔧 Zertifikat-Validierung
|
||||
|
||||
|
||||
@ -15,14 +15,14 @@ Ihr DOCX Template Server läuft jetzt mit **vollständiger SSL-Verschlüsselung*
|
||||
|
||||
#### HTTP (Port 80):
|
||||
- **Web-Interface:** http://localhost:80
|
||||
- **Templates:** http://localhost:80/webdav/templates/
|
||||
- **Dokumente:** http://localhost:80/webdav/documents/
|
||||
- **Templates:** http://localhost:80/templates/
|
||||
- **Dokumente:** http://localhost:80/documents/
|
||||
- **API:** http://localhost:80/api/
|
||||
|
||||
#### HTTPS (Port 443) - **NEU AKTIV**:
|
||||
- **Web-Interface:** https://localhost:443
|
||||
- **Templates:** https://localhost:443/webdav/templates/
|
||||
- **Dokumente:** https://localhost:443/webdav/documents/
|
||||
- **Templates:** https://localhost:443/templates/
|
||||
- **Dokumente:** https://localhost:443/documents/
|
||||
- **API:** https://localhost:443/api/
|
||||
|
||||
### 🚀 **Server-Features:**
|
||||
@ -31,7 +31,7 @@ Ihr DOCX Template Server läuft jetzt mit **vollständiger SSL-Verschlüsselung*
|
||||
- ✅ **DOCX-Template-Verarbeitung** mit automatischer Tag-Erkennung
|
||||
- ✅ **Demo-Daten-Generierung** für alle Tag-Typen
|
||||
- ✅ **Tabellen-Unterstützung** für Listen und Wiederholungen
|
||||
- ✅ **WebDAV-ähnliche Dateifreigabe** ohne Authentifizierung
|
||||
- ✅ **HTTP-Dateifreigabe** ohne Authentifizierung
|
||||
- ✅ **Vollständige API** für programmatischen Zugriff
|
||||
|
||||
### 📋 **Einsatz in der Produktion:**
|
||||
@ -103,7 +103,7 @@ curl -I -k https://localhost:443
|
||||
✅ **docxtemplater-Integration** mit intelligenter Tag-Erkennung
|
||||
✅ **Automatische Demo-Daten-Generierung**
|
||||
✅ **Tabellen- und Listen-Unterstützung**
|
||||
✅ **WebDAV-ähnliche Dateifreigabe** ohne Authentifizierung
|
||||
✅ **HTTP-Dateifreigabe** ohne Authentifizierung
|
||||
✅ **Produktionsbereit** mit vollständiger Dokumentation
|
||||
|
||||
**Ihr DOCX Template Server ist vollständig einsatzbereit und SSL-gesichert! 🔒✨**
|
||||
16
STATUS.md
16
STATUS.md
@ -8,14 +8,14 @@
|
||||
- ✅ **Intelligente Tag-Erkennung** - erkennt automatisch alle `{tags}` im Template
|
||||
- ✅ **Demo-Daten-Generierung** - füllt Tags mit realistischen Testdaten
|
||||
- ✅ **Tabellen-Unterstützung** - `{#items}...{/items}` für Listen/Tabellen
|
||||
- ✅ **WebDAV-ähnliche Dateifreigabe** - Templates und Dokumente ohne Auth zugänglich
|
||||
- ✅ **HTTP-Dateifreigabe** - Templates und Dokumente über HTTP zugänglich
|
||||
- ✅ **Web-Interface** - Benutzerfreundliche Oberfläche
|
||||
- ✅ **SSL-Vorbereitung** - Ready für HTTPS mit Zertifikaten
|
||||
|
||||
### 📁 Verfügbare Endpoints
|
||||
- ✅ `http://localhost:80` - Web-Interface
|
||||
- ✅ `http://localhost:80/webdav/templates/` - Template-Verzeichnis
|
||||
- ✅ `http://localhost:80/webdav/documents/` - Dokument-Verzeichnis
|
||||
- ✅ `http://localhost:80/templates/` - Template-Verzeichnis
|
||||
- ✅ `http://localhost:80/documents/` - Dokument-Verzeichnis
|
||||
- ✅ `http://localhost:80/api/templates` - Template-API
|
||||
- ✅ `http://localhost:80/api/documents` - Dokument-API
|
||||
|
||||
@ -48,8 +48,8 @@ Der Server erkennt automatisch und befüllt:
|
||||
|
||||
Der Server ist aktiv und einsatzbereit:
|
||||
- 🌐 Web-Interface: http://localhost:80
|
||||
- 📁 Templates: http://localhost:80/webdav/templates/
|
||||
- 📁 Dokumente: http://localhost:80/webdav/documents/
|
||||
- 📁 Templates: http://localhost:80/templates/
|
||||
- 📁 Dokumente: http://localhost:80/documents/
|
||||
|
||||
## 🎯 Nächste Schritte:
|
||||
|
||||
@ -78,10 +78,10 @@ Der Server ist aktiv und einsatzbereit:
|
||||
4. Fertiges Dokument herunterladen
|
||||
|
||||
### Upload via Dateifreigabe:
|
||||
1. Template in `http://localhost:80/webdav/templates/` kopieren
|
||||
1. Template in `http://localhost:80/templates/` hochladen
|
||||
2. Server erkennt Template automatisch
|
||||
3. Über API verarbeiten lassen
|
||||
4. Ergebnis aus `http://localhost:80/webdav/documents/` abholen
|
||||
4. Ergebnis aus `http://localhost:80/documents/` abholen
|
||||
|
||||
### API-Verwendung:
|
||||
```bash
|
||||
@ -101,7 +101,7 @@ curl -X POST -H "Content-Type: application/json" \
|
||||
✅ **Automatische Tag-Erkennung**
|
||||
✅ **Demo-Daten-Generierung**
|
||||
✅ **Tabellen-Unterstützung**
|
||||
✅ **WebDAV-ähnliche Dateifreigabe** ohne Auth
|
||||
✅ **HTTP-Dateifreigabe** ohne Auth
|
||||
✅ **SSL-Vorbereitung**
|
||||
✅ **Vollständige Dokumentation**
|
||||
|
||||
|
||||
@ -38,12 +38,12 @@
|
||||
### 🌐 **Zugriff auf Templates und Dokumente:**
|
||||
|
||||
**HTTP:**
|
||||
- Templates: http://localhost:80/webdav/templates/
|
||||
- Dokumente: http://localhost:80/webdav/documents/
|
||||
- Templates: http://localhost:80/templates/
|
||||
- Dokumente: http://localhost:80/documents/
|
||||
|
||||
**HTTPS:**
|
||||
- Templates: https://localhost:443/webdav/templates/
|
||||
- Dokumente: https://localhost:443/webdav/documents/
|
||||
- Templates: https://localhost:443/templates/
|
||||
- Dokumente: https://localhost:443/documents/
|
||||
|
||||
### 🧪 **Demo-Daten werden automatisch generiert:**
|
||||
- **Position:** 1, 2, 3, 4, 5... (fortlaufend)
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
# 🔗 WORD-WEBDAV-INTEGRATION EINGERICHTET!
|
||||
|
||||
## ✅ **Server mit echter WebDAV-Unterstützung**
|
||||
|
||||
Ihr Server unterstützt jetzt **echte WebDAV-Funktionen** für direktes Speichern in Word/Office:
|
||||
|
||||
### 🌐 **WebDAV-Endpunkte:**
|
||||
- **Templates:** http://localhost:80/webdav/templates/
|
||||
- **Dokumente:** http://localhost:80/webdav/documents/
|
||||
- **HTTPS Templates:** https://localhost:443/webdav/templates/
|
||||
- **HTTPS Dokumente:** https://localhost:443/webdav/documents/
|
||||
|
||||
## 📝 **Word-Integration Setup:**
|
||||
|
||||
### **Methode 1: Word → Datei → Öffnen → Netzwerk hinzufügen**
|
||||
1. Öffnen Sie Microsoft Word
|
||||
2. Gehen Sie zu **Datei** → **Öffnen**
|
||||
3. Klicken Sie auf **Netzwerk hinzufügen**
|
||||
4. Wählen Sie **WebDAV**
|
||||
5. Geben Sie ein: `http://localhost:80/webdav/templates/`
|
||||
6. **Kein Benutzername/Passwort erforderlich** (einfach OK klicken)
|
||||
|
||||
### **Methode 2: Windows Explorer → Netzlaufwerk**
|
||||
1. Öffnen Sie Windows Explorer
|
||||
2. Rechtsklick auf **Dieser PC**
|
||||
3. **Netzlaufwerk verbinden**
|
||||
4. Als Ordner eingeben: `http://localhost:80/webdav/templates/`
|
||||
5. **"Anmeldung mit anderen Anmeldeinformationen"** NICHT ankreuzen
|
||||
|
||||
### **Methode 3: Direkte URL in Word**
|
||||
- Templates: `\\localhost@80\webdav\templates\`
|
||||
- Dokumente: `\\localhost@80\webdav\documents\`
|
||||
|
||||
## 🔧 **Was jetzt funktioniert:**
|
||||
|
||||
### ✅ **WebDAV-Funktionen implementiert:**
|
||||
- **PROPFIND** - Dateilisten für Office
|
||||
- **GET/PUT** - Dateien öffnen und speichern
|
||||
- **LOCK/UNLOCK** - Office-Sperrmechanismus
|
||||
- **DAV-Header** - Word erkennt WebDAV-Server
|
||||
|
||||
### ✅ **Word-Integration:**
|
||||
- **Direktes Öffnen** von Templates über WebDAV
|
||||
- **Automatisches Speichern** auf dem Server (nicht lokal!)
|
||||
- **Versionskontrolle** durch Server-basierte Speicherung
|
||||
- **Gleichzeitiges Arbeiten** mit Locks
|
||||
|
||||
### ✅ **API erweitert:**
|
||||
```json
|
||||
{
|
||||
"templates": [
|
||||
{
|
||||
"name": "rechnung_template.docx",
|
||||
"fileUrl": "http://localhost:80/webdav/templates/rechnung_template.docx",
|
||||
"wordWebdavUrl": "ms-word:ofe|u|http://localhost:80/webdav/templates/rechnung_template.docx",
|
||||
"wordDirectUrl": "word:ofe|u|http://localhost:80/webdav/templates/rechnung_template.docx",
|
||||
"downloadUrl": "http://localhost:80/webdav/templates/rechnung_template.docx",
|
||||
"webdavDirect": "\\\\localhost@80\\webdav\\templates\\rechnung_template.docx"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 **So funktioniert das Speichern:**
|
||||
|
||||
### **Vorher (Problem):**
|
||||
- Word öffnet Datei → Bearbeiten → **Speichern geht nur lokal**
|
||||
|
||||
### **Jetzt (Lösung):**
|
||||
- Word öffnet über WebDAV → Bearbeiten → **Speichern geht direkt auf Server!**
|
||||
|
||||
## 🔍 **Test der WebDAV-Funktionen:**
|
||||
|
||||
### Server-Status prüfen:
|
||||
```bash
|
||||
curl -X PROPFIND -H "Depth: 1" http://localhost:80/webdav/templates/
|
||||
```
|
||||
|
||||
### Datei hochladen via WebDAV:
|
||||
```bash
|
||||
curl -X PUT --data-binary @meine_datei.docx http://localhost:80/webdav/templates/meine_datei.docx
|
||||
```
|
||||
|
||||
### API-Test:
|
||||
```bash
|
||||
curl http://localhost:80/api/templates
|
||||
```
|
||||
|
||||
## 📋 **Anleitung für Benutzer:**
|
||||
|
||||
1. **Word öffnen**
|
||||
2. **Datei → Öffnen → Netzwerk hinzufügen → WebDAV**
|
||||
3. **URL eingeben:** `http://localhost:80/webdav/templates/`
|
||||
4. **Template auswählen und öffnen**
|
||||
5. **Bearbeiten**
|
||||
6. **Strg+S drücken** → **Speichert automatisch auf dem Server!**
|
||||
|
||||
## 🚀 **Ergebnis:**
|
||||
|
||||
✅ **Echte WebDAV-Integration** für Office
|
||||
✅ **Direktes Speichern** auf dem Server
|
||||
✅ **Keine lokalen Kopien** mehr nötig
|
||||
✅ **Professioneller Workflow** für Teams
|
||||
✅ **Zentrale Datenverwaltung** auf dem Server
|
||||
|
||||
**Word speichert jetzt automatisch auf dem Server, wenn über WebDAV geöffnet! 🎉**
|
||||
BIN
documents/final-test-custom-tags.docx
Normal file
BIN
documents/final-test-custom-tags.docx
Normal file
Binary file not shown.
BIN
documents/test-all-custom-tags.docx
Normal file
BIN
documents/test-all-custom-tags.docx
Normal file
Binary file not shown.
BIN
documents/test-with-custom-tags.docx
Normal file
BIN
documents/test-with-custom-tags.docx
Normal file
Binary file not shown.
939
server_old.js
939
server_old.js
@ -1,939 +0,0 @@
|
||||
const express = require('express');
|
||||
const Docxtemplater = require('docxtemplater');
|
||||
const PizZip = require('pizzip');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const multer = require('multer');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { faker } = require('@faker-js/faker');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 80;
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Verzeichnisse erstellen
|
||||
const templateDir = path.join(__dirname, 'templates');
|
||||
const outputDir = path.join(__dirname, 'documents');
|
||||
|
||||
[templateDir, outputDir].forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
// WebDAV-Implementierung für Word/Office-Integration mit SharePoint-Kompatibilität
|
||||
const webdavMethods = ['PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK'];
|
||||
|
||||
// SharePoint-spezifische Namespaces und Header
|
||||
const sharePointNamespaces = {
|
||||
'xmlns:D': 'DAV:',
|
||||
'xmlns:S': 'http://schemas.microsoft.com/sharepoint/soap/',
|
||||
'xmlns:Z': 'urn:schemas-microsoft-com:',
|
||||
'xmlns:O': 'urn:schemas-microsoft-com:office:office',
|
||||
'xmlns:T': 'http://schemas.microsoft.com/repl/'
|
||||
};
|
||||
|
||||
// WebDAV PROPFIND Handler mit SharePoint-Modus
|
||||
app.use('/webdav', (req, res, next) => {
|
||||
// Erweiterte CORS und WebDAV-Header für Office/SharePoint
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning, extended-mkcol',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0 Microsoft-HTTPAPI/2.0',
|
||||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||||
'SPIisLatency': '0',
|
||||
'Allow': 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Authorization, Destination, If, Lock-Token, Overwrite, Timeout, X-Requested-With, SOAPAction',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
'X-SharePointHealthScore': '0',
|
||||
'X-AspNet-Version': '4.0.30319',
|
||||
'X-Powered-By': 'ASP.NET'
|
||||
});
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
if (req.method === 'PROPFIND') {
|
||||
const depth = req.headers.depth || 'infinity';
|
||||
const requestPath = req.path.replace('/webdav', '');
|
||||
|
||||
let targetDir;
|
||||
if (requestPath.startsWith('/templates')) {
|
||||
targetDir = path.join(templateDir, requestPath.replace('/templates', ''));
|
||||
} else if (requestPath.startsWith('/documents')) {
|
||||
targetDir = path.join(outputDir, requestPath.replace('/documents', ''));
|
||||
} else {
|
||||
targetDir = __dirname;
|
||||
}
|
||||
|
||||
try {
|
||||
let items = [];
|
||||
|
||||
if (fs.existsSync(targetDir)) {
|
||||
const stats = fs.statSync(targetDir);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const files = fs.readdirSync(targetDir);
|
||||
|
||||
// Verzeichnis selbst
|
||||
items.push({
|
||||
href: req.path + (req.path.endsWith('/') ? '' : '/'),
|
||||
isDirectory: true,
|
||||
lastModified: stats.mtime.toUTCString(),
|
||||
size: 0
|
||||
});
|
||||
|
||||
// Dateien im Verzeichnis
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(targetDir, file);
|
||||
const fileStats = fs.statSync(filePath);
|
||||
|
||||
items.push({
|
||||
href: req.path + (req.path.endsWith('/') ? '' : '/') + file,
|
||||
isDirectory: fileStats.isDirectory(),
|
||||
lastModified: fileStats.mtime.toUTCString(),
|
||||
size: fileStats.size || 0,
|
||||
contentType: fileStats.isDirectory() ? 'httpd/unix-directory' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Einzelne Datei
|
||||
items.push({
|
||||
href: req.path,
|
||||
isDirectory: false,
|
||||
lastModified: stats.mtime.toUTCString(),
|
||||
size: stats.size,
|
||||
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// WebDAV XML Response mit SharePoint-spezifischen Eigenschaften
|
||||
const xmlResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:multistatus xmlns:D="DAV:" xmlns:S="http://schemas.microsoft.com/sharepoint/soap/" xmlns:Z="urn:schemas-microsoft-com:" xmlns:O="urn:schemas-microsoft-com:office:office">
|
||||
${items.map(item => `
|
||||
<D:response>
|
||||
<D:href>${item.href}</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:displayname>${path.basename(item.href)}</D:displayname>
|
||||
<D:getlastmodified>${item.lastModified}</D:getlastmodified>
|
||||
<D:getcontentlength>${item.size}</D:getcontentlength>
|
||||
<D:getcontenttype>${item.contentType}</D:getcontenttype>
|
||||
<D:resourcetype>${item.isDirectory ? '<D:collection/>' : ''}</D:resourcetype>
|
||||
<D:supportedlock>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:shared/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
</D:supportedlock>
|
||||
<D:getetag>"${Date.now()}-${item.size}"</D:getetag>
|
||||
<D:creationdate>${new Date(item.lastModified).toISOString()}</D:creationdate>
|
||||
<D:iscollection>${item.isDirectory ? 'true' : 'false'}</D:iscollection>
|
||||
<D:executable>F</D:executable>
|
||||
<D:ishidden>F</D:ishidden>
|
||||
<D:isreadonly>F</D:isreadonly>
|
||||
<O:IsReadonly>F</O:IsReadonly>
|
||||
<Z:Win32CreationTime>${new Date(item.lastModified).toISOString()}</Z:Win32CreationTime>
|
||||
<Z:Win32LastAccessTime>${new Date().toISOString()}</Z:Win32LastAccessTime>
|
||||
<Z:Win32LastModifiedTime>${new Date(item.lastModified).toISOString()}</Z:Win32LastModifiedTime>
|
||||
<Z:Win32FileAttributes>00000020</Z:Win32FileAttributes>
|
||||
<S:modifiedby>System Account</S:modifiedby>
|
||||
<S:checkedoutby></S:checkedoutby>
|
||||
<S:IsCheckedoutToLocal>0</S:IsCheckedoutToLocal>
|
||||
<S:CheckoutExpires></S:CheckoutExpires>
|
||||
<S:CanCheckOut>1</S:CanCheckOut>
|
||||
<S:ContentTypeId>0x0101</S:ContentTypeId>
|
||||
<S:DocumentWorkflowStatus>0</S:DocumentWorkflowStatus>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>`).join('')}
|
||||
</D:multistatus>`;
|
||||
|
||||
res.set('Content-Type', 'application/xml; charset=utf-8');
|
||||
res.status(207).send(xmlResponse);
|
||||
} catch (error) {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// WebDAV PUT Handler für Dateien speichern
|
||||
// PUT - File Upload (für Word Speichern)
|
||||
app.put('/dav/*', (req, res) => {
|
||||
const filePath = path.join(__dirname, 'uploads', req.params[0]);
|
||||
const dirPath = path.dirname(filePath);
|
||||
|
||||
console.log(`<EFBFBD> PUT Request: ${req.params[0]}`);
|
||||
console.log(`📂 Speichere nach: ${filePath}`);
|
||||
|
||||
// Stelle sicher dass der Ordner existiert
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
|
||||
req.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
console.log(`✅ Datei gespeichert: ${filePath}`);
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning, extended-mkcol',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0 Microsoft-HTTPAPI/2.0',
|
||||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||||
'Cache-Control': 'no-cache',
|
||||
'ETag': `"${Date.now()}-${req.headers['content-length'] || 0}"`,
|
||||
'Last-Modified': new Date().toUTCString(),
|
||||
'X-SharePoint-HealthScore': '0'
|
||||
});
|
||||
res.status(201).send('Created');
|
||||
});
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
console.error(`❌ Fehler beim Speichern: ${err.message}`);
|
||||
res.status(500).send('Internal Server Error');
|
||||
});
|
||||
});
|
||||
|
||||
app.put('/webdav/documents/:filename', (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
const filePath = path.join(outputDir, filename);
|
||||
|
||||
console.log(`📝 Word speichert Dokument: ${filename}`);
|
||||
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
req.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
// Dateirechte explizit setzen
|
||||
try {
|
||||
fs.chmodSync(filePath, 0o666); // Lese-/Schreibrechte für alle
|
||||
} catch (error) {
|
||||
console.warn('Warnung: Konnte Dateiberechtigungen nicht setzen:', error.message);
|
||||
}
|
||||
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0',
|
||||
'Cache-Control': 'no-cache',
|
||||
'ETag': `"${Date.now()}-${req.headers['content-length'] || 0}"`,
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
res.status(201).send('Created');
|
||||
console.log(`✅ Dokument gespeichert: ${filename}`);
|
||||
});
|
||||
|
||||
writeStream.on('error', (error) => {
|
||||
console.error('❌ Fehler beim Speichern:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
});
|
||||
});
|
||||
|
||||
// WebDAV GET Handler für Dateien lesen
|
||||
app.get('/webdav/templates/:filename', (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
const filePath = path.join(templateDir, filename);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stats = fs.statSync(filePath);
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0',
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'Content-Length': stats.size,
|
||||
'Last-Modified': stats.mtime.toUTCString(),
|
||||
'ETag': `"${stats.mtime.getTime()}-${stats.size}"`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
console.log(`📖 Word öffnet Template: ${filename}`);
|
||||
res.sendFile(filePath);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/webdav/documents/:filename', (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
const filePath = path.join(outputDir, filename);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stats = fs.statSync(filePath);
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0',
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'Content-Length': stats.size,
|
||||
'Last-Modified': stats.mtime.toUTCString(),
|
||||
'ETag': `"${stats.mtime.getTime()}-${stats.size}"`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
console.log(`📖 Word öffnet Dokument: ${filename}`);
|
||||
res.sendFile(filePath);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
// WebDAV LOCK/UNLOCK für Office
|
||||
app.use('/webdav', (req, res, next) => {
|
||||
if (req.method === 'LOCK') {
|
||||
const lockToken = 'opaquelocktoken:' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
const lockResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:prop xmlns:D="DAV:">
|
||||
<D:lockdiscovery>
|
||||
<D:activelock>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:depth>0</D:depth>
|
||||
<D:timeout>Second-604800</D:timeout>
|
||||
<D:locktoken><D:href>${lockToken}</D:href></D:locktoken>
|
||||
</D:activelock>
|
||||
</D:lockdiscovery>
|
||||
</D:prop>`;
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'Lock-Token': `<${lockToken}>`,
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV'
|
||||
});
|
||||
res.status(200).send(lockResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'UNLOCK') {
|
||||
res.set({
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV'
|
||||
});
|
||||
res.status(204).send();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/webdav/documents', express.static(outputDir, {
|
||||
setHeaders: (res, path) => {
|
||||
res.set({
|
||||
'DAV': '1, 2',
|
||||
'Allow': 'GET, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, HEAD, OPTIONS',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, HEAD, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Authorization, If-Match, If-None-Match, Overwrite, Destination',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// WebDAV PROPFIND Support für Word
|
||||
app.use('/webdav/*', (req, res, next) => {
|
||||
if (req.method === 'PROPFIND') {
|
||||
res.set({
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV'
|
||||
});
|
||||
|
||||
const propfindResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:multistatus xmlns:D="DAV:">
|
||||
<D:response>
|
||||
<D:href>${req.originalUrl}</D:href>
|
||||
<D:propstat>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
<D:prop>
|
||||
<D:resourcetype><D:collection/></D:resourcetype>
|
||||
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
|
||||
<D:supportedlock>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
</D:supportedlock>
|
||||
</D:prop>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>`;
|
||||
|
||||
res.status(207).send(propfindResponse);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// SharePoint-spezifische Endpunkte
|
||||
app.get('/_vti_inf.html', (req, res) => {
|
||||
// SharePoint Info-Seite
|
||||
res.set('Content-Type', 'text/html');
|
||||
res.send(`<!-- FrontPage Configuration Information
|
||||
FP_VER:15.0.0.0000
|
||||
FP_SCHEMA_VER:15.0.0.0000
|
||||
-->`);
|
||||
});
|
||||
|
||||
app.post('/_vti_bin/lists.asmx', (req, res) => {
|
||||
// SharePoint Lists Web Service
|
||||
res.set({
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': req.headers.soapaction || ''
|
||||
});
|
||||
|
||||
const soapResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<soap:Body>
|
||||
<GetListCollectionResponse xmlns="http://schemas.microsoft.com/sharepoint/soap/">
|
||||
<GetListCollectionResult>
|
||||
<Lists>
|
||||
<List DocTemplateUrl="" DefaultViewUrl="/templates" ID="{${crypto.randomUUID()}}" Title="Templates" Description="Document Templates" ImageUrl="/_layouts/images/itdl.gif" Name="{${crypto.randomUUID()}}" BaseType="1" ServerTemplate="101" Created="2025-10-02T19:00:00Z" Modified="2025-10-02T19:00:00Z" LastDeleted="1900-01-01T00:00:00Z" Version="1" Direction="none" ThumbnailSize="0" WebImageWidth="0" WebImageHeight="0" Flags="0" ItemCount="3" AnonymousPermMask="0" RootFolder="/webdav/templates" ReadSecurity="1" WriteSecurity="1" Author="1" InheritedSecurity="true" AllowMultiResponses="false" AllowDeletion="true" AllowAnonymousAccess="false" EnableAttachments="true" EnableModeration="false" EnableVersioning="false" Hidden="false" MultipleDataList="false" Ordered="false" ShowUser="true" EnablePeopleSelector="false" EnableResourceSelector="false" EnableMinorVersion="false" RequireCheckout="false" />
|
||||
</Lists>
|
||||
</GetListCollectionResult>
|
||||
</GetListCollectionResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>`;
|
||||
|
||||
res.send(soapResponse);
|
||||
});
|
||||
|
||||
app.get('/_vti_bin/owssvr.dll', (req, res) => {
|
||||
// SharePoint OWS Service
|
||||
if (req.query.Cmd === 'Display' && req.query.List) {
|
||||
res.set('Content-Type', 'text/xml');
|
||||
res.send(`<?xml version="1.0" encoding="utf-8"?>
|
||||
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
|
||||
<s:Schema id="RowsetSchema">
|
||||
<s:ElementType name="row" content="eltOnly" rs:updatable="true">
|
||||
<s:AttributeType name="ows_Title" rs:name="Title" rs:number="1"/>
|
||||
<s:AttributeType name="ows_Modified" rs:name="Modified" rs:number="2"/>
|
||||
<s:AttributeType name="ows_Editor" rs:name="Editor" rs:number="3"/>
|
||||
</s:ElementType>
|
||||
</s:Schema>
|
||||
<rs:data>
|
||||
<z:row ows_Title="Templates" ows_Modified="2025-10-02T19:00:00Z" ows_Editor="System Account"/>
|
||||
</rs:data>
|
||||
</xml>`);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
// SharePoint-kompatible WebDAV-Root
|
||||
app.use('/sharepoint', (req, res, next) => {
|
||||
// SharePoint-spezifische Header
|
||||
res.set({
|
||||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||||
'SharePointHealthScore': '0',
|
||||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||||
'X-SharePoint-HealthScore': '0'
|
||||
});
|
||||
|
||||
// Weiterleitung zu WebDAV
|
||||
const newPath = req.path.replace('/sharepoint', '/webdav');
|
||||
req.url = newPath;
|
||||
next();
|
||||
});
|
||||
|
||||
console.log('SharePoint-Kompatibilitätsmodus aktiviert:');
|
||||
console.log('SharePoint WebDAV: http://localhost:' + (process.env.PORT || 80) + '/sharepoint/templates/');
|
||||
console.log('SharePoint Info: http://localhost:' + (process.env.PORT || 80) + '/_vti_inf.html');
|
||||
|
||||
// Multer für File Upload
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
|
||||
// Demo-Daten Generator
|
||||
class DemoDataGenerator {
|
||||
static generateData(tags) {
|
||||
const data = {};
|
||||
|
||||
tags.forEach(tag => {
|
||||
const lowerTag = tag.toLowerCase();
|
||||
|
||||
if (lowerTag.includes('name') || lowerTag.includes('vorname')) {
|
||||
data[tag] = faker.person.firstName();
|
||||
} else if (lowerTag.includes('nachname') || lowerTag.includes('surname')) {
|
||||
data[tag] = faker.person.lastName();
|
||||
} else if (lowerTag.includes('email') || lowerTag.includes('mail')) {
|
||||
data[tag] = faker.internet.email();
|
||||
} else if (lowerTag.includes('telefon') || lowerTag.includes('phone')) {
|
||||
data[tag] = faker.phone.number();
|
||||
} else if (lowerTag.includes('adresse') || lowerTag.includes('address')) {
|
||||
data[tag] = faker.location.streetAddress();
|
||||
} else if (lowerTag.includes('stadt') || lowerTag.includes('city')) {
|
||||
data[tag] = faker.location.city();
|
||||
} else if (lowerTag.includes('plz') || lowerTag.includes('postal')) {
|
||||
data[tag] = faker.location.zipCode();
|
||||
} else if (lowerTag.includes('land') || lowerTag.includes('country')) {
|
||||
data[tag] = faker.location.country();
|
||||
} else if (lowerTag.includes('datum') || lowerTag.includes('date')) {
|
||||
data[tag] = faker.date.recent().toLocaleDateString('de-DE');
|
||||
} else if (lowerTag.includes('betrag') || lowerTag.includes('preis') || lowerTag.includes('amount')) {
|
||||
data[tag] = faker.commerce.price();
|
||||
} else if (lowerTag.includes('firma') || lowerTag.includes('company')) {
|
||||
data[tag] = faker.company.name();
|
||||
} else if (lowerTag.includes('produkt') || lowerTag.includes('product')) {
|
||||
data[tag] = faker.commerce.productName();
|
||||
} else if (lowerTag.includes('beschreibung') || lowerTag.includes('description')) {
|
||||
data[tag] = faker.lorem.paragraph();
|
||||
} else if (lowerTag.includes('nummer') || lowerTag.includes('number') || lowerTag.includes('id')) {
|
||||
data[tag] = faker.string.numeric(6);
|
||||
} else {
|
||||
// Fallback für unbekannte Tags
|
||||
data[tag] = faker.lorem.words(2);
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static generateTableData(tableStructure) {
|
||||
const rows = Math.floor(Math.random() * 5) + 2; // 2-6 Zeilen
|
||||
const tableData = [];
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const row = {};
|
||||
tableStructure.forEach(column => {
|
||||
if (column.includes('position')) {
|
||||
row[column] = (i + 1).toString(); // Fortlaufende Positionsnummer
|
||||
} else {
|
||||
row[column] = this.generateData([column])[column];
|
||||
}
|
||||
});
|
||||
tableData.push(row);
|
||||
}
|
||||
|
||||
return tableData;
|
||||
}
|
||||
}
|
||||
|
||||
// Template Tag Extractor
|
||||
class TemplateTagExtractor {
|
||||
static extractTags(docxBuffer) {
|
||||
try {
|
||||
const zip = new PizZip(docxBuffer);
|
||||
const doc = new Docxtemplater(zip, {
|
||||
paragraphLoop: true,
|
||||
linebreaks: true,
|
||||
});
|
||||
|
||||
// Alle Tags aus dem Template extrahieren
|
||||
const tags = new Set();
|
||||
const content = zip.file('word/document.xml').asText();
|
||||
|
||||
// Einfache Tags: {tag}
|
||||
const simpleTagRegex = /{([^{}]+)}/g;
|
||||
let match;
|
||||
while ((match = simpleTagRegex.exec(content)) !== null) {
|
||||
const tag = match[1].trim();
|
||||
if (!tag.includes('#') && !tag.includes('/')) {
|
||||
tags.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop Tags für Tabellen: {#items}...{/items}
|
||||
const loopTagRegex = /{#(\w+)}/g;
|
||||
const loopTags = [];
|
||||
while ((match = loopTagRegex.exec(content)) !== null) {
|
||||
loopTags.push(match[1]);
|
||||
}
|
||||
|
||||
return {
|
||||
simpleTags: Array.from(tags),
|
||||
loopTags: loopTags
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Extrahieren der Tags:', error);
|
||||
return { simpleTags: [], loopTags: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Template Processor
|
||||
class TemplateProcessor {
|
||||
static async processTemplate(templatePath, outputPath, customData = null) {
|
||||
try {
|
||||
const content = fs.readFileSync(templatePath, 'binary');
|
||||
const zip = new PizZip(content);
|
||||
|
||||
// Tags extrahieren
|
||||
const { simpleTags, loopTags } = TemplateTagExtractor.extractTags(Buffer.from(content, 'binary'));
|
||||
|
||||
// Daten generieren oder verwenden
|
||||
let data = customData || {};
|
||||
|
||||
if (!customData) {
|
||||
// Demo-Daten für einfache Tags generieren
|
||||
data = DemoDataGenerator.generateData(simpleTags);
|
||||
|
||||
// Demo-Daten für Loop Tags (Tabellen) generieren
|
||||
loopTags.forEach(loopTag => {
|
||||
// Erweiterte Tabellen-Spalten für professionelle Rechnung
|
||||
const tableColumns = [`${loopTag}_position`, `${loopTag}_name`, `${loopTag}_value`, `${loopTag}_date`];
|
||||
data[loopTag] = DemoDataGenerator.generateTableData(tableColumns);
|
||||
});
|
||||
}
|
||||
|
||||
const doc = new Docxtemplater(zip, {
|
||||
paragraphLoop: true,
|
||||
linebreaks: true,
|
||||
});
|
||||
|
||||
doc.render(data);
|
||||
|
||||
const buf = doc.getZip().generate({
|
||||
type: 'nodebuffer',
|
||||
compression: 'DEFLATE',
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputPath, buf);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
extractedTags: { simpleTags, loopTags }
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Template Verarbeitung fehlgeschlagen:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Routes
|
||||
|
||||
// Hauptseite
|
||||
app.get('/', (req, res) => {
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DOCX Template Server</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||||
.container { max-width: 800px; }
|
||||
.section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
.button { background: #007cba; color: white; padding: 10px 20px; text-decoration: none; border-radius: 3px; display: inline-block; margin: 5px; }
|
||||
.button:hover { background: #005a87; }
|
||||
input[type="file"] { margin: 10px 0; }
|
||||
.info { background: #f0f8ff; padding: 10px; border-left: 4px solid #007cba; margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>DOCX Template Server</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>📄 Template hochladen und verarbeiten</h2>
|
||||
<form action="/upload-template" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="template" accept=".docx" required>
|
||||
<br>
|
||||
<input type="submit" value="Template hochladen und verarbeiten" class="button">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📁 Office-Integration</h2>
|
||||
<div class="info">
|
||||
<strong>Word WebDAV Templates:</strong> <a href="/webdav/templates/" target="_blank">http://localhost:${PORT}/webdav/templates/</a><br>
|
||||
<strong>Word WebDAV Dokumente:</strong> <a href="/webdav/documents/" target="_blank">http://localhost:${PORT}/webdav/documents/</a><br>
|
||||
${fs.existsSync(path.join(__dirname, '203_key.pem')) && fs.existsSync(path.join(__dirname, '203_cert.pem')) ?
|
||||
`<strong>Templates (HTTPS):</strong> <a href="https://localhost:443/webdav/templates/" target="_blank">https://localhost:443/webdav/templates/</a><br>
|
||||
<strong>Dokumente (HTTPS):</strong> <a href="https://localhost:443/webdav/documents/" target="_blank">https://localhost:443/webdav/documents/</a>` :
|
||||
'<em>HTTPS verfügbar sobald SSL-Zertifikate (203_cert.pem und 203_key.pem) im Projektverzeichnis vorhanden sind</em>'}
|
||||
</div>
|
||||
<p><strong>Office-Integration:</strong> Öffnen Sie in Word/Excel: Datei → Öffnen → Netzwerk hinzufügen → WebDAV → http://localhost:${PORT}/webdav/templates/</p>
|
||||
<p><strong>Direkt speichern:</strong> Word speichert automatisch auf dem Server wenn über WebDAV geöffnet.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🔧 API Endpunkte</h2>
|
||||
<ul>
|
||||
<li><a href="/api/templates" class="button">Templates auflisten</a></li>
|
||||
<li><a href="/api/documents" class="button">Dokumente auflisten</a></li>
|
||||
</ul>
|
||||
<p><strong>Word-Integration:</strong> Die API liefert spezielle Links für Microsoft Word:</p>
|
||||
<ul>
|
||||
<li><code>wordWebdavUrl</code> - Direkt in Word öffnen (ms-word: Protocol)</li>
|
||||
<li><code>wordDirectUrl</code> - Alternative Word-Integration</li>
|
||||
<li><code>webdavDirect</code> - Windows UNC-Pfad für Netzlaufwerk</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📋 Test Template</h2>
|
||||
<a href="/create-test-template" class="button">Test Template erstellen</a>
|
||||
<p>Erstellt ein Beispiel-Template mit verschiedenen Tag-Typen für Tests.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// Template Upload und Verarbeitung
|
||||
app.post('/upload-template', upload.single('template'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Keine Datei hochgeladen' });
|
||||
}
|
||||
|
||||
const templateName = req.file.originalname;
|
||||
const templatePath = path.join(templateDir, templateName);
|
||||
const outputName = templateName.replace('.docx', '_ausgefuellt.docx');
|
||||
const outputPath = path.join(outputDir, outputName);
|
||||
|
||||
// Template in Templates-Verzeichnis kopieren
|
||||
fs.copyFileSync(req.file.path, templatePath);
|
||||
|
||||
// Template verarbeiten
|
||||
const result = await TemplateProcessor.processTemplate(templatePath, outputPath);
|
||||
|
||||
// Upload-Datei löschen
|
||||
fs.unlinkSync(req.file.path);
|
||||
|
||||
if (result.success) {
|
||||
const protocol = req.secure ? 'https' : 'http';
|
||||
const host = req.get('host') || `localhost:${PORT}`;
|
||||
|
||||
res.json({
|
||||
message: 'Template erfolgreich verarbeitet',
|
||||
templateName: templateName,
|
||||
outputName: outputName,
|
||||
extractedTags: result.extractedTags,
|
||||
generatedData: result.data,
|
||||
fileUrls: {
|
||||
template: `${protocol}://${host}/webdav/templates/${templateName}`,
|
||||
document: `${protocol}://${host}/webdav/documents/${outputName}`,
|
||||
wordWebdavTemplate: `ms-word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(templateName)}`,
|
||||
wordWebdavDocument: `ms-word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(outputName)}`,
|
||||
wordDirectTemplate: `word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(templateName)}`,
|
||||
wordDirectDocument: `word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(outputName)}`
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: result.error });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload Fehler:', error);
|
||||
res.status(500).json({ error: 'Server Fehler beim Upload' });
|
||||
}
|
||||
});
|
||||
|
||||
// Test Template erstellen
|
||||
app.get('/create-test-template', (req, res) => {
|
||||
const PizZip = require('pizzip');
|
||||
|
||||
// Minimales DOCX Template erstellen
|
||||
const testContent = `
|
||||
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:body>
|
||||
<w:p><w:r><w:t>Firmenname: {firma}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Ansprechpartner: {vorname} {nachname}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>E-Mail: {email}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Telefon: {telefon}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Adresse: {adresse}, {plz} {stadt}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Datum: {datum}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Rechnungsnummer: {nummer}</w:t></w:r></w:p>
|
||||
|
||||
<w:p><w:r><w:t>Positionen:</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>{#items}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>- {items_name}: {items_value} EUR ({items_date})</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>{/items}</w:t></w:r></w:p>
|
||||
|
||||
<w:p><w:r><w:t>Gesamtbetrag: {betrag} EUR</w:t></w:r></w:p>
|
||||
|
||||
<w:p><w:r><w:t>Beschreibung:</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>{beschreibung}</w:t></w:r></w:p>
|
||||
</w:body>
|
||||
</w:document>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Minimal DOCX Struktur
|
||||
const zip = new PizZip();
|
||||
|
||||
// document.xml
|
||||
zip.file('word/document.xml', testContent);
|
||||
|
||||
// [Content_Types].xml
|
||||
zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||
</Types>`);
|
||||
|
||||
// _rels/.rels
|
||||
zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||
</Relationships>`);
|
||||
|
||||
// word/_rels/document.xml.rels
|
||||
zip.file('word/_rels/document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
</Relationships>`);
|
||||
|
||||
const buffer = zip.generate({ type: 'nodebuffer' });
|
||||
const testTemplatePath = path.join(templateDir, 'test_template.docx');
|
||||
|
||||
fs.writeFileSync(testTemplatePath, buffer);
|
||||
|
||||
res.json({
|
||||
message: 'Test Template erstellt',
|
||||
templatePath: 'test_template.docx',
|
||||
fileUrl: `http://localhost:${PORT}/webdav/templates/test_template.docx`,
|
||||
info: 'Sie können das Template jetzt über den Datei-Server herunterladen, bearbeiten und wieder hochladen.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Test Templates:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Erstellen des Test Templates' });
|
||||
}
|
||||
});
|
||||
|
||||
// API Endpunkte
|
||||
app.get('/api/templates', (req, res) => {
|
||||
try {
|
||||
const files = fs.readdirSync(templateDir).filter(file => file.endsWith('.docx'));
|
||||
const protocol = req.secure ? 'https' : 'http';
|
||||
const host = req.get('host') || `localhost:${PORT}`;
|
||||
|
||||
res.json({
|
||||
templates: files.map(file => ({
|
||||
name: file,
|
||||
fileUrl: `${protocol}://${host}/webdav/templates/${file}`,
|
||||
wordWebdavUrl: `ms-word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(file)}`,
|
||||
wordDirectUrl: `word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(file)}`,
|
||||
downloadUrl: `${protocol}://${host}/webdav/templates/${file}`,
|
||||
webdavDirect: `\\\\${host.split(':')[0]}@${PORT}\\webdav\\templates\\${file}`
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Fehler beim Auflisten der Templates' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/documents', (req, res) => {
|
||||
try {
|
||||
const files = fs.readdirSync(outputDir).filter(file => file.endsWith('.docx'));
|
||||
const protocol = req.secure ? 'https' : 'http';
|
||||
const host = req.get('host') || `localhost:${PORT}`;
|
||||
|
||||
res.json({
|
||||
documents: files.map(file => ({
|
||||
name: file,
|
||||
fileUrl: `${protocol}://${host}/webdav/documents/${file}`,
|
||||
wordWebdavUrl: `ms-word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(file)}`,
|
||||
wordDirectUrl: `word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(file)}`,
|
||||
downloadUrl: `${protocol}://${host}/webdav/documents/${file}`,
|
||||
webdavDirect: `\\\\${host.split(':')[0]}@${PORT}\\webdav\\documents\\${file}`,
|
||||
createdAt: fs.statSync(path.join(outputDir, file)).mtime
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Fehler beim Auflisten der Dokumente' });
|
||||
}
|
||||
});
|
||||
|
||||
// Template mit benutzerdefinierten Daten verarbeiten
|
||||
app.post('/api/process-template/:templateName', (req, res) => {
|
||||
try {
|
||||
const templateName = req.params.templateName;
|
||||
const templatePath = path.join(templateDir, templateName);
|
||||
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
return res.status(404).json({ error: 'Template nicht gefunden' });
|
||||
}
|
||||
|
||||
const outputName = templateName.replace('.docx', '_custom.docx');
|
||||
const outputPath = path.join(outputDir, outputName);
|
||||
|
||||
TemplateProcessor.processTemplate(templatePath, outputPath, req.body).then(result => {
|
||||
if (result.success) {
|
||||
res.json({
|
||||
message: 'Template mit benutzerdefinierten Daten verarbeitet',
|
||||
outputName: outputName,
|
||||
fileUrl: `http://localhost:${PORT}/webdav/documents/${outputName}`
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: result.error });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Fehler beim Verarbeiten des Templates' });
|
||||
}
|
||||
});
|
||||
|
||||
// SSL Konfiguration
|
||||
const certPath = path.join(__dirname, '203_cert.pem');
|
||||
const keyPath = path.join(__dirname, '203_key.pem');
|
||||
|
||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||
try {
|
||||
const sslOptions = {
|
||||
key: fs.readFileSync(keyPath),
|
||||
cert: fs.readFileSync(certPath)
|
||||
};
|
||||
|
||||
https.createServer(sslOptions, app).listen(443, () => {
|
||||
console.log('🔒 HTTPS Server läuft auf Port 443');
|
||||
console.log('🌐 HTTPS Zugang: https://localhost:443');
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('⚠️ SSL-Zertifikate gefunden, aber Fehler beim Laden:', error.message);
|
||||
console.log('ℹ️ Server läuft nur mit HTTP');
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ SSL-Zertifikate nicht gefunden - Server läuft nur mit HTTP');
|
||||
console.log('💡 Für HTTPS: Platzieren Sie 203_cert.pem und 203_key.pem in /home/OfficeServerJS/');
|
||||
}
|
||||
|
||||
// Server starten
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 DOCX Template Server gestartet!`);
|
||||
console.log(`📍 HTTP Server: http://localhost:${PORT}`);
|
||||
console.log(`📁 Templates: http://localhost:${PORT}/webdav/templates/`);
|
||||
console.log(`📁 Documents: http://localhost:${PORT}/webdav/documents/`);
|
||||
console.log(`\n💡 Tipp: Besuchen Sie http://localhost:${PORT} für die Web-Oberfläche`);
|
||||
|
||||
// SSL-Status anzeigen
|
||||
const certPath = path.join(__dirname, '203_cert.pem');
|
||||
const keyPath = path.join(__dirname, '203_key.pem');
|
||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||
console.log(`🔒 HTTPS auch verfügbar: https://localhost:443`);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@ -1,939 +0,0 @@
|
||||
const express = require('express');
|
||||
const Docxtemplater = require('docxtemplater');
|
||||
const PizZip = require('pizzip');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const multer = require('multer');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { faker } = require('@faker-js/faker');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 80;
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Verzeichnisse erstellen
|
||||
const templateDir = path.join(__dirname, 'templates');
|
||||
const outputDir = path.join(__dirname, 'documents');
|
||||
|
||||
[templateDir, outputDir].forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
// WebDAV-Implementierung für Word/Office-Integration mit SharePoint-Kompatibilität
|
||||
const webdavMethods = ['PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK'];
|
||||
|
||||
// SharePoint-spezifische Namespaces und Header
|
||||
const sharePointNamespaces = {
|
||||
'xmlns:D': 'DAV:',
|
||||
'xmlns:S': 'http://schemas.microsoft.com/sharepoint/soap/',
|
||||
'xmlns:Z': 'urn:schemas-microsoft-com:',
|
||||
'xmlns:O': 'urn:schemas-microsoft-com:office:office',
|
||||
'xmlns:T': 'http://schemas.microsoft.com/repl/'
|
||||
};
|
||||
|
||||
// WebDAV PROPFIND Handler mit SharePoint-Modus
|
||||
app.use('/webdav', (req, res, next) => {
|
||||
// Erweiterte CORS und WebDAV-Header für Office/SharePoint
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning, extended-mkcol',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0 Microsoft-HTTPAPI/2.0',
|
||||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||||
'SPIisLatency': '0',
|
||||
'Allow': 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Authorization, Destination, If, Lock-Token, Overwrite, Timeout, X-Requested-With, SOAPAction',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
'X-SharePointHealthScore': '0',
|
||||
'X-AspNet-Version': '4.0.30319',
|
||||
'X-Powered-By': 'ASP.NET'
|
||||
});
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
if (req.method === 'PROPFIND') {
|
||||
const depth = req.headers.depth || 'infinity';
|
||||
const requestPath = req.path.replace('/webdav', '');
|
||||
|
||||
let targetDir;
|
||||
if (requestPath.startsWith('/templates')) {
|
||||
targetDir = path.join(templateDir, requestPath.replace('/templates', ''));
|
||||
} else if (requestPath.startsWith('/documents')) {
|
||||
targetDir = path.join(outputDir, requestPath.replace('/documents', ''));
|
||||
} else {
|
||||
targetDir = __dirname;
|
||||
}
|
||||
|
||||
try {
|
||||
let items = [];
|
||||
|
||||
if (fs.existsSync(targetDir)) {
|
||||
const stats = fs.statSync(targetDir);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const files = fs.readdirSync(targetDir);
|
||||
|
||||
// Verzeichnis selbst
|
||||
items.push({
|
||||
href: req.path + (req.path.endsWith('/') ? '' : '/'),
|
||||
isDirectory: true,
|
||||
lastModified: stats.mtime.toUTCString(),
|
||||
size: 0
|
||||
});
|
||||
|
||||
// Dateien im Verzeichnis
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(targetDir, file);
|
||||
const fileStats = fs.statSync(filePath);
|
||||
|
||||
items.push({
|
||||
href: req.path + (req.path.endsWith('/') ? '' : '/') + file,
|
||||
isDirectory: fileStats.isDirectory(),
|
||||
lastModified: fileStats.mtime.toUTCString(),
|
||||
size: fileStats.size || 0,
|
||||
contentType: fileStats.isDirectory() ? 'httpd/unix-directory' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Einzelne Datei
|
||||
items.push({
|
||||
href: req.path,
|
||||
isDirectory: false,
|
||||
lastModified: stats.mtime.toUTCString(),
|
||||
size: stats.size,
|
||||
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// WebDAV XML Response mit SharePoint-spezifischen Eigenschaften
|
||||
const xmlResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:multistatus xmlns:D="DAV:" xmlns:S="http://schemas.microsoft.com/sharepoint/soap/" xmlns:Z="urn:schemas-microsoft-com:" xmlns:O="urn:schemas-microsoft-com:office:office">
|
||||
${items.map(item => `
|
||||
<D:response>
|
||||
<D:href>${item.href}</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:displayname>${path.basename(item.href)}</D:displayname>
|
||||
<D:getlastmodified>${item.lastModified}</D:getlastmodified>
|
||||
<D:getcontentlength>${item.size}</D:getcontentlength>
|
||||
<D:getcontenttype>${item.contentType}</D:getcontenttype>
|
||||
<D:resourcetype>${item.isDirectory ? '<D:collection/>' : ''}</D:resourcetype>
|
||||
<D:supportedlock>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:shared/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
</D:supportedlock>
|
||||
<D:getetag>"${Date.now()}-${item.size}"</D:getetag>
|
||||
<D:creationdate>${new Date(item.lastModified).toISOString()}</D:creationdate>
|
||||
<D:iscollection>${item.isDirectory ? 'true' : 'false'}</D:iscollection>
|
||||
<D:executable>F</D:executable>
|
||||
<D:ishidden>F</D:ishidden>
|
||||
<D:isreadonly>F</D:isreadonly>
|
||||
<O:IsReadonly>F</O:IsReadonly>
|
||||
<Z:Win32CreationTime>${new Date(item.lastModified).toISOString()}</Z:Win32CreationTime>
|
||||
<Z:Win32LastAccessTime>${new Date().toISOString()}</Z:Win32LastAccessTime>
|
||||
<Z:Win32LastModifiedTime>${new Date(item.lastModified).toISOString()}</Z:Win32LastModifiedTime>
|
||||
<Z:Win32FileAttributes>00000020</Z:Win32FileAttributes>
|
||||
<S:modifiedby>System Account</S:modifiedby>
|
||||
<S:checkedoutby></S:checkedoutby>
|
||||
<S:IsCheckedoutToLocal>0</S:IsCheckedoutToLocal>
|
||||
<S:CheckoutExpires></S:CheckoutExpires>
|
||||
<S:CanCheckOut>1</S:CanCheckOut>
|
||||
<S:ContentTypeId>0x0101</S:ContentTypeId>
|
||||
<S:DocumentWorkflowStatus>0</S:DocumentWorkflowStatus>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>`).join('')}
|
||||
</D:multistatus>`;
|
||||
|
||||
res.set('Content-Type', 'application/xml; charset=utf-8');
|
||||
res.status(207).send(xmlResponse);
|
||||
} catch (error) {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// WebDAV PUT Handler für Dateien speichern
|
||||
// PUT - File Upload (für Word Speichern)
|
||||
app.put('/dav/*', (req, res) => {
|
||||
const filePath = path.join(__dirname, 'uploads', req.params[0]);
|
||||
const dirPath = path.dirname(filePath);
|
||||
|
||||
console.log(`<EFBFBD> PUT Request: ${req.params[0]}`);
|
||||
console.log(`📂 Speichere nach: ${filePath}`);
|
||||
|
||||
// Stelle sicher dass der Ordner existiert
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
|
||||
req.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
console.log(`✅ Datei gespeichert: ${filePath}`);
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning, extended-mkcol',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0 Microsoft-HTTPAPI/2.0',
|
||||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||||
'Cache-Control': 'no-cache',
|
||||
'ETag': `"${Date.now()}-${req.headers['content-length'] || 0}"`,
|
||||
'Last-Modified': new Date().toUTCString(),
|
||||
'X-SharePoint-HealthScore': '0'
|
||||
});
|
||||
res.status(201).send('Created');
|
||||
});
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
console.error(`❌ Fehler beim Speichern: ${err.message}`);
|
||||
res.status(500).send('Internal Server Error');
|
||||
});
|
||||
});
|
||||
|
||||
app.put('/webdav/documents/:filename', (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
const filePath = path.join(outputDir, filename);
|
||||
|
||||
console.log(`📝 Word speichert Dokument: ${filename}`);
|
||||
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
req.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
// Dateirechte explizit setzen
|
||||
try {
|
||||
fs.chmodSync(filePath, 0o666); // Lese-/Schreibrechte für alle
|
||||
} catch (error) {
|
||||
console.warn('Warnung: Konnte Dateiberechtigungen nicht setzen:', error.message);
|
||||
}
|
||||
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0',
|
||||
'Cache-Control': 'no-cache',
|
||||
'ETag': `"${Date.now()}-${req.headers['content-length'] || 0}"`,
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
res.status(201).send('Created');
|
||||
console.log(`✅ Dokument gespeichert: ${filename}`);
|
||||
});
|
||||
|
||||
writeStream.on('error', (error) => {
|
||||
console.error('❌ Fehler beim Speichern:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
});
|
||||
});
|
||||
|
||||
// WebDAV GET Handler für Dateien lesen
|
||||
app.get('/webdav/templates/:filename', (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
const filePath = path.join(templateDir, filename);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stats = fs.statSync(filePath);
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0',
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'Content-Length': stats.size,
|
||||
'Last-Modified': stats.mtime.toUTCString(),
|
||||
'ETag': `"${stats.mtime.getTime()}-${stats.size}"`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
console.log(`📖 Word öffnet Template: ${filename}`);
|
||||
res.sendFile(filePath);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/webdav/documents/:filename', (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
const filePath = path.join(outputDir, filename);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stats = fs.statSync(filePath);
|
||||
res.set({
|
||||
'DAV': '1, 2, ordered-collections, versioning',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Server': 'Microsoft-IIS/10.0',
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'Content-Length': stats.size,
|
||||
'Last-Modified': stats.mtime.toUTCString(),
|
||||
'ETag': `"${stats.mtime.getTime()}-${stats.size}"`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
console.log(`📖 Word öffnet Dokument: ${filename}`);
|
||||
res.sendFile(filePath);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
// WebDAV LOCK/UNLOCK für Office
|
||||
app.use('/webdav', (req, res, next) => {
|
||||
if (req.method === 'LOCK') {
|
||||
const lockToken = 'opaquelocktoken:' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
const lockResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:prop xmlns:D="DAV:">
|
||||
<D:lockdiscovery>
|
||||
<D:activelock>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:depth>0</D:depth>
|
||||
<D:timeout>Second-604800</D:timeout>
|
||||
<D:locktoken><D:href>${lockToken}</D:href></D:locktoken>
|
||||
</D:activelock>
|
||||
</D:lockdiscovery>
|
||||
</D:prop>`;
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'Lock-Token': `<${lockToken}>`,
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV'
|
||||
});
|
||||
res.status(200).send(lockResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'UNLOCK') {
|
||||
res.set({
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV'
|
||||
});
|
||||
res.status(204).send();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/webdav/documents', express.static(outputDir, {
|
||||
setHeaders: (res, path) => {
|
||||
res.set({
|
||||
'DAV': '1, 2',
|
||||
'Allow': 'GET, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, HEAD, OPTIONS',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, HEAD, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Authorization, If-Match, If-None-Match, Overwrite, Destination',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// WebDAV PROPFIND Support für Word
|
||||
app.use('/webdav/*', (req, res, next) => {
|
||||
if (req.method === 'PROPFIND') {
|
||||
res.set({
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV'
|
||||
});
|
||||
|
||||
const propfindResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:multistatus xmlns:D="DAV:">
|
||||
<D:response>
|
||||
<D:href>${req.originalUrl}</D:href>
|
||||
<D:propstat>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
<D:prop>
|
||||
<D:resourcetype><D:collection/></D:resourcetype>
|
||||
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
|
||||
<D:supportedlock>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
</D:supportedlock>
|
||||
</D:prop>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>`;
|
||||
|
||||
res.status(207).send(propfindResponse);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// SharePoint-spezifische Endpunkte
|
||||
app.get('/_vti_inf.html', (req, res) => {
|
||||
// SharePoint Info-Seite
|
||||
res.set('Content-Type', 'text/html');
|
||||
res.send(`<!-- FrontPage Configuration Information
|
||||
FP_VER:15.0.0.0000
|
||||
FP_SCHEMA_VER:15.0.0.0000
|
||||
-->`);
|
||||
});
|
||||
|
||||
app.post('/_vti_bin/lists.asmx', (req, res) => {
|
||||
// SharePoint Lists Web Service
|
||||
res.set({
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': req.headers.soapaction || ''
|
||||
});
|
||||
|
||||
const soapResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<soap:Body>
|
||||
<GetListCollectionResponse xmlns="http://schemas.microsoft.com/sharepoint/soap/">
|
||||
<GetListCollectionResult>
|
||||
<Lists>
|
||||
<List DocTemplateUrl="" DefaultViewUrl="/templates" ID="{${crypto.randomUUID()}}" Title="Templates" Description="Document Templates" ImageUrl="/_layouts/images/itdl.gif" Name="{${crypto.randomUUID()}}" BaseType="1" ServerTemplate="101" Created="2025-10-02T19:00:00Z" Modified="2025-10-02T19:00:00Z" LastDeleted="1900-01-01T00:00:00Z" Version="1" Direction="none" ThumbnailSize="0" WebImageWidth="0" WebImageHeight="0" Flags="0" ItemCount="3" AnonymousPermMask="0" RootFolder="/webdav/templates" ReadSecurity="1" WriteSecurity="1" Author="1" InheritedSecurity="true" AllowMultiResponses="false" AllowDeletion="true" AllowAnonymousAccess="false" EnableAttachments="true" EnableModeration="false" EnableVersioning="false" Hidden="false" MultipleDataList="false" Ordered="false" ShowUser="true" EnablePeopleSelector="false" EnableResourceSelector="false" EnableMinorVersion="false" RequireCheckout="false" />
|
||||
</Lists>
|
||||
</GetListCollectionResult>
|
||||
</GetListCollectionResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>`;
|
||||
|
||||
res.send(soapResponse);
|
||||
});
|
||||
|
||||
app.get('/_vti_bin/owssvr.dll', (req, res) => {
|
||||
// SharePoint OWS Service
|
||||
if (req.query.Cmd === 'Display' && req.query.List) {
|
||||
res.set('Content-Type', 'text/xml');
|
||||
res.send(`<?xml version="1.0" encoding="utf-8"?>
|
||||
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
|
||||
<s:Schema id="RowsetSchema">
|
||||
<s:ElementType name="row" content="eltOnly" rs:updatable="true">
|
||||
<s:AttributeType name="ows_Title" rs:name="Title" rs:number="1"/>
|
||||
<s:AttributeType name="ows_Modified" rs:name="Modified" rs:number="2"/>
|
||||
<s:AttributeType name="ows_Editor" rs:name="Editor" rs:number="3"/>
|
||||
</s:ElementType>
|
||||
</s:Schema>
|
||||
<rs:data>
|
||||
<z:row ows_Title="Templates" ows_Modified="2025-10-02T19:00:00Z" ows_Editor="System Account"/>
|
||||
</rs:data>
|
||||
</xml>`);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
// SharePoint-kompatible WebDAV-Root
|
||||
app.use('/sharepoint', (req, res, next) => {
|
||||
// SharePoint-spezifische Header
|
||||
res.set({
|
||||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||||
'SharePointHealthScore': '0',
|
||||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||||
'X-SharePoint-HealthScore': '0'
|
||||
});
|
||||
|
||||
// Weiterleitung zu WebDAV
|
||||
const newPath = req.path.replace('/sharepoint', '/webdav');
|
||||
req.url = newPath;
|
||||
next();
|
||||
});
|
||||
|
||||
console.log('SharePoint-Kompatibilitätsmodus aktiviert:');
|
||||
console.log('SharePoint WebDAV: http://localhost:' + (process.env.PORT || 80) + '/sharepoint/templates/');
|
||||
console.log('SharePoint Info: http://localhost:' + (process.env.PORT || 80) + '/_vti_inf.html');
|
||||
|
||||
// Multer für File Upload
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
|
||||
// Demo-Daten Generator
|
||||
class DemoDataGenerator {
|
||||
static generateData(tags) {
|
||||
const data = {};
|
||||
|
||||
tags.forEach(tag => {
|
||||
const lowerTag = tag.toLowerCase();
|
||||
|
||||
if (lowerTag.includes('name') || lowerTag.includes('vorname')) {
|
||||
data[tag] = faker.person.firstName();
|
||||
} else if (lowerTag.includes('nachname') || lowerTag.includes('surname')) {
|
||||
data[tag] = faker.person.lastName();
|
||||
} else if (lowerTag.includes('email') || lowerTag.includes('mail')) {
|
||||
data[tag] = faker.internet.email();
|
||||
} else if (lowerTag.includes('telefon') || lowerTag.includes('phone')) {
|
||||
data[tag] = faker.phone.number();
|
||||
} else if (lowerTag.includes('adresse') || lowerTag.includes('address')) {
|
||||
data[tag] = faker.location.streetAddress();
|
||||
} else if (lowerTag.includes('stadt') || lowerTag.includes('city')) {
|
||||
data[tag] = faker.location.city();
|
||||
} else if (lowerTag.includes('plz') || lowerTag.includes('postal')) {
|
||||
data[tag] = faker.location.zipCode();
|
||||
} else if (lowerTag.includes('land') || lowerTag.includes('country')) {
|
||||
data[tag] = faker.location.country();
|
||||
} else if (lowerTag.includes('datum') || lowerTag.includes('date')) {
|
||||
data[tag] = faker.date.recent().toLocaleDateString('de-DE');
|
||||
} else if (lowerTag.includes('betrag') || lowerTag.includes('preis') || lowerTag.includes('amount')) {
|
||||
data[tag] = faker.commerce.price();
|
||||
} else if (lowerTag.includes('firma') || lowerTag.includes('company')) {
|
||||
data[tag] = faker.company.name();
|
||||
} else if (lowerTag.includes('produkt') || lowerTag.includes('product')) {
|
||||
data[tag] = faker.commerce.productName();
|
||||
} else if (lowerTag.includes('beschreibung') || lowerTag.includes('description')) {
|
||||
data[tag] = faker.lorem.paragraph();
|
||||
} else if (lowerTag.includes('nummer') || lowerTag.includes('number') || lowerTag.includes('id')) {
|
||||
data[tag] = faker.string.numeric(6);
|
||||
} else {
|
||||
// Fallback für unbekannte Tags
|
||||
data[tag] = faker.lorem.words(2);
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static generateTableData(tableStructure) {
|
||||
const rows = Math.floor(Math.random() * 5) + 2; // 2-6 Zeilen
|
||||
const tableData = [];
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const row = {};
|
||||
tableStructure.forEach(column => {
|
||||
if (column.includes('position')) {
|
||||
row[column] = (i + 1).toString(); // Fortlaufende Positionsnummer
|
||||
} else {
|
||||
row[column] = this.generateData([column])[column];
|
||||
}
|
||||
});
|
||||
tableData.push(row);
|
||||
}
|
||||
|
||||
return tableData;
|
||||
}
|
||||
}
|
||||
|
||||
// Template Tag Extractor
|
||||
class TemplateTagExtractor {
|
||||
static extractTags(docxBuffer) {
|
||||
try {
|
||||
const zip = new PizZip(docxBuffer);
|
||||
const doc = new Docxtemplater(zip, {
|
||||
paragraphLoop: true,
|
||||
linebreaks: true,
|
||||
});
|
||||
|
||||
// Alle Tags aus dem Template extrahieren
|
||||
const tags = new Set();
|
||||
const content = zip.file('word/document.xml').asText();
|
||||
|
||||
// Einfache Tags: {tag}
|
||||
const simpleTagRegex = /{([^{}]+)}/g;
|
||||
let match;
|
||||
while ((match = simpleTagRegex.exec(content)) !== null) {
|
||||
const tag = match[1].trim();
|
||||
if (!tag.includes('#') && !tag.includes('/')) {
|
||||
tags.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop Tags für Tabellen: {#items}...{/items}
|
||||
const loopTagRegex = /{#(\w+)}/g;
|
||||
const loopTags = [];
|
||||
while ((match = loopTagRegex.exec(content)) !== null) {
|
||||
loopTags.push(match[1]);
|
||||
}
|
||||
|
||||
return {
|
||||
simpleTags: Array.from(tags),
|
||||
loopTags: loopTags
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Extrahieren der Tags:', error);
|
||||
return { simpleTags: [], loopTags: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Template Processor
|
||||
class TemplateProcessor {
|
||||
static async processTemplate(templatePath, outputPath, customData = null) {
|
||||
try {
|
||||
const content = fs.readFileSync(templatePath, 'binary');
|
||||
const zip = new PizZip(content);
|
||||
|
||||
// Tags extrahieren
|
||||
const { simpleTags, loopTags } = TemplateTagExtractor.extractTags(Buffer.from(content, 'binary'));
|
||||
|
||||
// Daten generieren oder verwenden
|
||||
let data = customData || {};
|
||||
|
||||
if (!customData) {
|
||||
// Demo-Daten für einfache Tags generieren
|
||||
data = DemoDataGenerator.generateData(simpleTags);
|
||||
|
||||
// Demo-Daten für Loop Tags (Tabellen) generieren
|
||||
loopTags.forEach(loopTag => {
|
||||
// Erweiterte Tabellen-Spalten für professionelle Rechnung
|
||||
const tableColumns = [`${loopTag}_position`, `${loopTag}_name`, `${loopTag}_value`, `${loopTag}_date`];
|
||||
data[loopTag] = DemoDataGenerator.generateTableData(tableColumns);
|
||||
});
|
||||
}
|
||||
|
||||
const doc = new Docxtemplater(zip, {
|
||||
paragraphLoop: true,
|
||||
linebreaks: true,
|
||||
});
|
||||
|
||||
doc.render(data);
|
||||
|
||||
const buf = doc.getZip().generate({
|
||||
type: 'nodebuffer',
|
||||
compression: 'DEFLATE',
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputPath, buf);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
extractedTags: { simpleTags, loopTags }
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Template Verarbeitung fehlgeschlagen:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Routes
|
||||
|
||||
// Hauptseite
|
||||
app.get('/', (req, res) => {
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DOCX Template Server</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||||
.container { max-width: 800px; }
|
||||
.section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
.button { background: #007cba; color: white; padding: 10px 20px; text-decoration: none; border-radius: 3px; display: inline-block; margin: 5px; }
|
||||
.button:hover { background: #005a87; }
|
||||
input[type="file"] { margin: 10px 0; }
|
||||
.info { background: #f0f8ff; padding: 10px; border-left: 4px solid #007cba; margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>DOCX Template Server</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>📄 Template hochladen und verarbeiten</h2>
|
||||
<form action="/upload-template" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="template" accept=".docx" required>
|
||||
<br>
|
||||
<input type="submit" value="Template hochladen und verarbeiten" class="button">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📁 Office-Integration</h2>
|
||||
<div class="info">
|
||||
<strong>Word WebDAV Templates:</strong> <a href="/webdav/templates/" target="_blank">http://localhost:${PORT}/webdav/templates/</a><br>
|
||||
<strong>Word WebDAV Dokumente:</strong> <a href="/webdav/documents/" target="_blank">http://localhost:${PORT}/webdav/documents/</a><br>
|
||||
${fs.existsSync(path.join(__dirname, '203_key.pem')) && fs.existsSync(path.join(__dirname, '203_cert.pem')) ?
|
||||
`<strong>Templates (HTTPS):</strong> <a href="https://localhost:443/webdav/templates/" target="_blank">https://localhost:443/webdav/templates/</a><br>
|
||||
<strong>Dokumente (HTTPS):</strong> <a href="https://localhost:443/webdav/documents/" target="_blank">https://localhost:443/webdav/documents/</a>` :
|
||||
'<em>HTTPS verfügbar sobald SSL-Zertifikate (203_cert.pem und 203_key.pem) im Projektverzeichnis vorhanden sind</em>'}
|
||||
</div>
|
||||
<p><strong>Office-Integration:</strong> Öffnen Sie in Word/Excel: Datei → Öffnen → Netzwerk hinzufügen → WebDAV → http://localhost:${PORT}/webdav/templates/</p>
|
||||
<p><strong>Direkt speichern:</strong> Word speichert automatisch auf dem Server wenn über WebDAV geöffnet.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🔧 API Endpunkte</h2>
|
||||
<ul>
|
||||
<li><a href="/api/templates" class="button">Templates auflisten</a></li>
|
||||
<li><a href="/api/documents" class="button">Dokumente auflisten</a></li>
|
||||
</ul>
|
||||
<p><strong>Word-Integration:</strong> Die API liefert spezielle Links für Microsoft Word:</p>
|
||||
<ul>
|
||||
<li><code>wordWebdavUrl</code> - Direkt in Word öffnen (ms-word: Protocol)</li>
|
||||
<li><code>wordDirectUrl</code> - Alternative Word-Integration</li>
|
||||
<li><code>webdavDirect</code> - Windows UNC-Pfad für Netzlaufwerk</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📋 Test Template</h2>
|
||||
<a href="/create-test-template" class="button">Test Template erstellen</a>
|
||||
<p>Erstellt ein Beispiel-Template mit verschiedenen Tag-Typen für Tests.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// Template Upload und Verarbeitung
|
||||
app.post('/upload-template', upload.single('template'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Keine Datei hochgeladen' });
|
||||
}
|
||||
|
||||
const templateName = req.file.originalname;
|
||||
const templatePath = path.join(templateDir, templateName);
|
||||
const outputName = templateName.replace('.docx', '_ausgefuellt.docx');
|
||||
const outputPath = path.join(outputDir, outputName);
|
||||
|
||||
// Template in Templates-Verzeichnis kopieren
|
||||
fs.copyFileSync(req.file.path, templatePath);
|
||||
|
||||
// Template verarbeiten
|
||||
const result = await TemplateProcessor.processTemplate(templatePath, outputPath);
|
||||
|
||||
// Upload-Datei löschen
|
||||
fs.unlinkSync(req.file.path);
|
||||
|
||||
if (result.success) {
|
||||
const protocol = req.secure ? 'https' : 'http';
|
||||
const host = req.get('host') || `localhost:${PORT}`;
|
||||
|
||||
res.json({
|
||||
message: 'Template erfolgreich verarbeitet',
|
||||
templateName: templateName,
|
||||
outputName: outputName,
|
||||
extractedTags: result.extractedTags,
|
||||
generatedData: result.data,
|
||||
fileUrls: {
|
||||
template: `${protocol}://${host}/webdav/templates/${templateName}`,
|
||||
document: `${protocol}://${host}/webdav/documents/${outputName}`,
|
||||
wordWebdavTemplate: `ms-word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(templateName)}`,
|
||||
wordWebdavDocument: `ms-word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(outputName)}`,
|
||||
wordDirectTemplate: `word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(templateName)}`,
|
||||
wordDirectDocument: `word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(outputName)}`
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: result.error });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload Fehler:', error);
|
||||
res.status(500).json({ error: 'Server Fehler beim Upload' });
|
||||
}
|
||||
});
|
||||
|
||||
// Test Template erstellen
|
||||
app.get('/create-test-template', (req, res) => {
|
||||
const PizZip = require('pizzip');
|
||||
|
||||
// Minimales DOCX Template erstellen
|
||||
const testContent = `
|
||||
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:body>
|
||||
<w:p><w:r><w:t>Firmenname: {firma}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Ansprechpartner: {vorname} {nachname}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>E-Mail: {email}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Telefon: {telefon}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Adresse: {adresse}, {plz} {stadt}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Datum: {datum}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>Rechnungsnummer: {nummer}</w:t></w:r></w:p>
|
||||
|
||||
<w:p><w:r><w:t>Positionen:</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>{#items}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>- {items_name}: {items_value} EUR ({items_date})</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>{/items}</w:t></w:r></w:p>
|
||||
|
||||
<w:p><w:r><w:t>Gesamtbetrag: {betrag} EUR</w:t></w:r></w:p>
|
||||
|
||||
<w:p><w:r><w:t>Beschreibung:</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>{beschreibung}</w:t></w:r></w:p>
|
||||
</w:body>
|
||||
</w:document>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Minimal DOCX Struktur
|
||||
const zip = new PizZip();
|
||||
|
||||
// document.xml
|
||||
zip.file('word/document.xml', testContent);
|
||||
|
||||
// [Content_Types].xml
|
||||
zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||
</Types>`);
|
||||
|
||||
// _rels/.rels
|
||||
zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||
</Relationships>`);
|
||||
|
||||
// word/_rels/document.xml.rels
|
||||
zip.file('word/_rels/document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
</Relationships>`);
|
||||
|
||||
const buffer = zip.generate({ type: 'nodebuffer' });
|
||||
const testTemplatePath = path.join(templateDir, 'test_template.docx');
|
||||
|
||||
fs.writeFileSync(testTemplatePath, buffer);
|
||||
|
||||
res.json({
|
||||
message: 'Test Template erstellt',
|
||||
templatePath: 'test_template.docx',
|
||||
fileUrl: `http://localhost:${PORT}/webdav/templates/test_template.docx`,
|
||||
info: 'Sie können das Template jetzt über den Datei-Server herunterladen, bearbeiten und wieder hochladen.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Test Templates:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Erstellen des Test Templates' });
|
||||
}
|
||||
});
|
||||
|
||||
// API Endpunkte
|
||||
app.get('/api/templates', (req, res) => {
|
||||
try {
|
||||
const files = fs.readdirSync(templateDir).filter(file => file.endsWith('.docx'));
|
||||
const protocol = req.secure ? 'https' : 'http';
|
||||
const host = req.get('host') || `localhost:${PORT}`;
|
||||
|
||||
res.json({
|
||||
templates: files.map(file => ({
|
||||
name: file,
|
||||
fileUrl: `${protocol}://${host}/webdav/templates/${file}`,
|
||||
wordWebdavUrl: `ms-word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(file)}`,
|
||||
wordDirectUrl: `word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(file)}`,
|
||||
downloadUrl: `${protocol}://${host}/webdav/templates/${file}`,
|
||||
webdavDirect: `\\\\${host.split(':')[0]}@${PORT}\\webdav\\templates\\${file}`
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Fehler beim Auflisten der Templates' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/documents', (req, res) => {
|
||||
try {
|
||||
const files = fs.readdirSync(outputDir).filter(file => file.endsWith('.docx'));
|
||||
const protocol = req.secure ? 'https' : 'http';
|
||||
const host = req.get('host') || `localhost:${PORT}`;
|
||||
|
||||
res.json({
|
||||
documents: files.map(file => ({
|
||||
name: file,
|
||||
fileUrl: `${protocol}://${host}/webdav/documents/${file}`,
|
||||
wordWebdavUrl: `ms-word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(file)}`,
|
||||
wordDirectUrl: `word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(file)}`,
|
||||
downloadUrl: `${protocol}://${host}/webdav/documents/${file}`,
|
||||
webdavDirect: `\\\\${host.split(':')[0]}@${PORT}\\webdav\\documents\\${file}`,
|
||||
createdAt: fs.statSync(path.join(outputDir, file)).mtime
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Fehler beim Auflisten der Dokumente' });
|
||||
}
|
||||
});
|
||||
|
||||
// Template mit benutzerdefinierten Daten verarbeiten
|
||||
app.post('/api/process-template/:templateName', (req, res) => {
|
||||
try {
|
||||
const templateName = req.params.templateName;
|
||||
const templatePath = path.join(templateDir, templateName);
|
||||
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
return res.status(404).json({ error: 'Template nicht gefunden' });
|
||||
}
|
||||
|
||||
const outputName = templateName.replace('.docx', '_custom.docx');
|
||||
const outputPath = path.join(outputDir, outputName);
|
||||
|
||||
TemplateProcessor.processTemplate(templatePath, outputPath, req.body).then(result => {
|
||||
if (result.success) {
|
||||
res.json({
|
||||
message: 'Template mit benutzerdefinierten Daten verarbeitet',
|
||||
outputName: outputName,
|
||||
fileUrl: `http://localhost:${PORT}/webdav/documents/${outputName}`
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: result.error });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Fehler beim Verarbeiten des Templates' });
|
||||
}
|
||||
});
|
||||
|
||||
// SSL Konfiguration
|
||||
const certPath = path.join(__dirname, '203_cert.pem');
|
||||
const keyPath = path.join(__dirname, '203_key.pem');
|
||||
|
||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||
try {
|
||||
const sslOptions = {
|
||||
key: fs.readFileSync(keyPath),
|
||||
cert: fs.readFileSync(certPath)
|
||||
};
|
||||
|
||||
https.createServer(sslOptions, app).listen(443, () => {
|
||||
console.log('🔒 HTTPS Server läuft auf Port 443');
|
||||
console.log('🌐 HTTPS Zugang: https://localhost:443');
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('⚠️ SSL-Zertifikate gefunden, aber Fehler beim Laden:', error.message);
|
||||
console.log('ℹ️ Server läuft nur mit HTTP');
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ SSL-Zertifikate nicht gefunden - Server läuft nur mit HTTP');
|
||||
console.log('💡 Für HTTPS: Platzieren Sie 203_cert.pem und 203_key.pem in /home/OfficeServerJS/');
|
||||
}
|
||||
|
||||
// Server starten
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 DOCX Template Server gestartet!`);
|
||||
console.log(`📍 HTTP Server: http://localhost:${PORT}`);
|
||||
console.log(`📁 Templates: http://localhost:${PORT}/webdav/templates/`);
|
||||
console.log(`📁 Documents: http://localhost:${PORT}/webdav/documents/`);
|
||||
console.log(`\n💡 Tipp: Besuchen Sie http://localhost:${PORT} für die Web-Oberfläche`);
|
||||
|
||||
// SSL-Status anzeigen
|
||||
const certPath = path.join(__dirname, '203_cert.pem');
|
||||
const keyPath = path.join(__dirname, '203_key.pem');
|
||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||
console.log(`🔒 HTTPS auch verfügbar: https://localhost:443`);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@ -50,10 +50,10 @@ echo " HTTP: http://localhost:80"
|
||||
echo " HTTPS: https://localhost:443"
|
||||
echo ""
|
||||
echo "📁 Dateifreigabe:"
|
||||
echo " HTTP Templates: http://localhost:80/webdav/templates/"
|
||||
echo " HTTPS Templates: https://localhost:443/webdav/templates/"
|
||||
echo " HTTP Documents: http://localhost:80/webdav/documents/"
|
||||
echo " HTTPS Documents: https://localhost:443/webdav/documents/"
|
||||
echo " HTTP Templates: http://localhost:80/templates/"
|
||||
echo " HTTPS Templates: https://localhost:443/templates/"
|
||||
echo " HTTP Documents: http://localhost:80/documents/"
|
||||
echo " HTTPS Documents: https://localhost:443/documents/"
|
||||
echo ""
|
||||
|
||||
# Server mit SSL starten
|
||||
|
||||
4
start.sh
4
start.sh
@ -28,8 +28,8 @@ fi
|
||||
echo "🌟 Starte den DOCX Template Server..."
|
||||
echo ""
|
||||
echo "📍 Web-Oberfläche: http://localhost:80"
|
||||
echo "📁 Templates: http://localhost:80/webdav/templates/"
|
||||
echo "📁 Dokumente: http://localhost:80/webdav/documents/"
|
||||
echo "📁 Templates: http://localhost:80/templates/"
|
||||
echo "📁 Dokumente: http://localhost:80/documents/"
|
||||
echo ""
|
||||
echo "💡 Drücken Sie Ctrl+C zum Beenden"
|
||||
echo ""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user