# Dateien aus der Nextcloud in d4rk_media uebernehmen. # # Aufruf: # pwsh -File tools\uebernehmen.ps1 -Von 'CDN/brands' -Nach brands # pwsh -File tools\uebernehmen.ps1 -Von 'CDN/items' -Nach items -Probe # # -Von Pfad IN der Nextcloud, relativ zum eigenen Ordner # -Nach Zielordner im Dienst. Leer = der letzte Teil von -Von # -Probe nur zeigen, nichts hochladen # # UNTERORDNER BLEIBEN ERHALTEN: aus CDN/brands/logos/x.png wird # brands/logos/x.png. Wer das nicht will, sortiert vorher in der Nextcloud um. # # WARUM UEBER DAS DASHBOARD UND NICHT UEBER DIE TOKEN-API. Nextcloud-Dateien # heissen "Brand Logo (final).PNG" -- checkPath lehnt das mit Recht ab, denn # Klammern und Leerzeichen haben in einer oeffentlichen Adresse nichts zu # suchen. Der Dashboard-Weg biegt den Namen bereits VORHERSAGBAR zurecht # (brand-logo-final.png). Dieselbe Regel an einer Stelle ist besser als # dieselbe Regel an zweien, von denen eine irgendwann abweicht. # # WIEDERHOLBAR: hochgeladen wird mit overwrite=false. Was schon da ist, meldet # der Dienst je Datei als "Pfad ist belegt" -- der Lauf laesst sich also # jederzeit abbrechen und noch einmal starten, ohne 500 Dateien erneut zu # schieben. [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$Von, [string]$Nach = '', [switch]$Probe, [string]$Dienst = 'https://fivemanage.d4rkst3r.de', [string]$Container = 'd4rk-media', [int]$ProStapel = 20 ) $ErrorActionPreference = 'Stop' function Schritt($t) { Write-Host "==> $t" -ForegroundColor Cyan } function Gut($t) { Write-Host " $t" -ForegroundColor Green } function Schlecht($t) { Write-Host " $t" -ForegroundColor Red } if (-not $Nach) { $Nach = ($Von.TrimEnd('/') -split '/')[-1] } $Nach = $Nach.Trim('/') # -- Zugang zur Nextcloud ------------------------------------------------------ # # Dieselbe Quelle wie bei der Sicherung: die Werte stehen im Panel unter # Einstellungen und werden hier direkt aus der Datenbank gelesen. So gibt es # EINEN Ort fuer diese Zugangsdaten und nicht zwei. $roh = docker exec $Container node -e @" const db = require('better-sqlite3')('/data/media.db', { readonly: true }); const hol = (k) => { const r = db.prepare('SELECT value FROM settings WHERE key = ?').get(k); return r ? r.value : ''; }; console.log(JSON.stringify({ basis: hol('sicherung.basis'), benutzer: hol('sicherung.benutzer'), passwort: hol('sicherung.passwort'), })); "@ if ($LASTEXITCODE -ne 0) { Schlecht 'Der Container antwortet nicht.'; exit 1 } $nc = $roh | ConvertFrom-Json if (-not ($nc.basis -and $nc.benutzer -and $nc.passwort)) { Schlecht 'Keine Nextcloud hinterlegt. Panel -> Einstellungen -> Sicherung ausfuellen.' exit 1 } $wurzel = "$($nc.basis.TrimEnd('/'))/remote.php/dav/files/$($nc.benutzer)" $auth = "$($nc.benutzer):$($nc.passwort)" # -- Was liegt dort ------------------------------------------------------------ Schritt "In der Nextcloud nachsehen: $Von" # Depth: infinity nennt auch alles in Unterordnern. Manche Server verweigern # das; dann bleibt nur Ebene fuer Ebene, und das sagen wir auch. # Das -join ist NICHT Kosmetik. curl.exe gibt seine Antwort zeilenweise als # ARRAY zurueck, und -match/-notmatch auf einem Array FILTERT, statt wahr oder # falsch zu liefern: ein nicht-leeres Ergebnis gilt als wahr, und damit schlug # die Pruefung unten immer an -- bei einer tadellosen Antwort mit HTTP 207. $xml = (curl.exe -s -u $auth -X PROPFIND -H 'Depth: infinity' "$wurzel/$Von") -join "`n" if (-not $xml -or $xml -notmatch '<[dD]:multistatus') { Schlecht "Kein Verzeichnis gefunden. Antwort: $($xml -replace '\s+', ' ' | Select-Object -First 1)" Schlecht 'Stimmt der Pfad? Er ist relativ zum eigenen Nextcloud-Ordner, ohne fuehrenden Schraegstrich.' exit 1 } # Die Antwort ist XML mit Namensraumpraefix (d: oder D:). Statt einen Parser # darauf anzusetzen, holen wir die beiden Felder, auf die es ankommt. $eintraege = @() foreach ($m in [regex]::Matches($xml, '(?s)<[dD]:response>(.*?)')) { $block = $m.Groups[1].Value if ($block -match '<[dD]:collection\s*/>') { continue } # Ordner selbst if ($block -notmatch '<[dD]:href>(.*?)') { continue } $href = [uri]::UnescapeDataString($Matches[1]) $groesse = if ($block -match '<[dD]:getcontentlength>(\d+)<') { [long]$Matches[1] } else { 0 } # Der href traegt den vollen DAV-Pfad. Wir wollen den Teil UNTERHALB von # -Von, damit die Unterordner erhalten bleiben. $praefix = "/remote.php/dav/files/$($nc.benutzer)/$Von/" if ($href -like "*$praefix*") { $rel = $href.Substring($href.IndexOf($praefix) + $praefix.Length) if ($rel) { $eintraege += [pscustomobject]@{ rel = $rel; href = $href; size = $groesse } } } } if ($eintraege.Count -eq 0) { Schlecht 'Keine Dateien gefunden.'; exit 1 } $summe = ($eintraege | Measure-Object -Property size -Sum).Sum Gut "$($eintraege.Count) Dateien, $([math]::Round($summe/1MB,1)) MB" # Nach Zielordner gruppieren: der Dashboard-Weg nimmt EINEN Ordner je Anfrage. $gruppen = $eintraege | Group-Object { $teile = $_.rel -split '/' if ($teile.Count -gt 1) { "$Nach/" + ($teile[0..($teile.Count - 2)] -join '/') } else { $Nach } } Write-Host '' foreach ($g in $gruppen) { Write-Host " $($g.Name)" -NoNewline -ForegroundColor DarkGray Write-Host " $($g.Count) Dateien" } Write-Host '' if ($Probe) { Gut 'Probelauf — nichts hochgeladen. Ohne -Probe geht es wirklich los.' exit 0 } # -- Anmelden am Dienst -------------------------------------------------------- $envDatei = Join-Path (Split-Path $PSScriptRoot -Parent) '.env' if (-not (Test-Path $envDatei)) { Schlecht "Keine .env neben dem Compose gefunden."; exit 1 } $pw = (Select-String -Path $envDatei -Pattern '^ADMIN_PASSWORD=(.+)$').Matches.Groups[1].Value $benutzer = 'admin' $m = Select-String -Path $envDatei -Pattern '^ADMIN_USERNAME=(.+)$' if ($m) { $benutzer = $m.Matches.Groups[1].Value } $keksglas = Join-Path $env:TEMP ('keks-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) $anmeldung = @{ username = $benutzer; password = $pw } | ConvertTo-Json -Compress $code = $anmeldung | curl.exe -s -o NUL -w '%{http_code}' -c $keksglas ` -X POST "$Dienst/api/dash/auth/login" -H 'content-type: application/json' -d '@-' if ($code -ne '200') { Schlecht "Anmeldung am Dienst fehlgeschlagen (HTTP $code)."; exit 1 } # -- Uebertragen --------------------------------------------------------------- $tmp = Join-Path $env:TEMP ('uebernahme-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) New-Item -ItemType Directory -Force -Path $tmp | Out-Null $hoch = 0; $schon = 0; $fehler = 0 foreach ($g in $gruppen) { $zielordner = $g.Name Schritt "$zielordner ($($g.Count) Dateien)" foreach ($stapel in ($g.Group | ForEach-Object -Begin { $i = 0; $puffer = @() } -Process { $puffer += $_ if ($puffer.Count -ge $ProStapel) { , $puffer; $puffer = @() } } -End { if ($puffer.Count) { , $puffer } })) { $dateien = @() foreach ($e in $stapel) { $name = ($e.rel -split '/')[-1] $lokal = Join-Path $tmp $name curl.exe -s -u $auth -o $lokal "$($nc.basis.TrimEnd('/'))$($e.href)" if (Test-Path $lokal) { $dateien += Get-Item $lokal } } if (-not $dateien) { continue } # HOCHGELADEN WIRD MIT CURL UND NICHT MIT Invoke-RestMethod -Form. # # Nicht aus Geschmack: .NET schreibt im Formular-Rumpf # "Content-Disposition: form-data; name=file" OHNE Anfuehrungszeichen, # RFC 7578 verlangt sie, und Nodes Parser besteht darauf. Der Dienst # antwortet dann mit "Der Formular-Rumpf liess sich nicht lesen". # Mitgeschnitten und nachgemessen -- curl setzt beides korrekt. $argumente = @( '-s', '-b', $keksglas, '-X', 'POST', "$Dienst/api/dash/upload", '-F', "folder=$zielordner", '-F', 'overwrite=false' ) foreach ($d in $dateien) { $argumente += @('-F', "file=@$($d.FullName)") } try { $res = (& curl.exe @argumente) | ConvertFrom-Json foreach ($r in $res.ergebnisse) { if ($r.ok) { $hoch++; Gut "$($r.path)" } elseif ($r.error -like '*belegt*') { $schon++ } else { $fehler++; Schlecht "$($r.name): $($r.error)" } } } catch { $fehler += $dateien.Count Schlecht "Stapel fehlgeschlagen: $($_.Exception.Message)" } $dateien | Remove-Item -Force -ErrorAction SilentlyContinue } } Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $keksglas -Force -ErrorAction SilentlyContinue Write-Host '' Gut "Fertig: $hoch uebernommen, $schon waren schon da, $fehler Fehler" if ($schon -gt 0) { Write-Host " (Ein zweiter Lauf laedt nichts doppelt — das ist Absicht.)" -ForegroundColor DarkGray }