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,32 @@
---
tags:
- Exchange Online
- PowerShell
- Scripting
---
## Purpose
Sometimes you will need to connect to Office365 via powershell in order to perform troubleshooting / automation that either is too complex to do via the website, or is not exposed / possible to do via the website.
## Update Nuget Package Manager
```powershell
Install-PackageProvider -Name NuGet -Force -ForceBootstrap
```
## Install ExchangeOnlineManagement Powershell Modules
You will need to install and import the modules for Exchange Online before you can run the commands necessary for interacting with it.
```powershell
Install-Module -Name ExchangeOnlineManagement -Force
Import-Module ExchangeOnlineManagement
```
## Connect to Exchange Online
When you run the following command, it will open a dialog box to take the username, password, and MFA code (if applicable) for an administrative account in the Exchange Online environment.
```powershell
Connect-ExchangeOnline -UserPrincipalName admin@domain.com
```
## Related Documentation
- [Related Email Documentation](<../../../../reference/Applications/Email/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,31 @@
---
tags:
- Microsoft Exchange
- Email
---
## Purpose
Sometimes you need to set an autoreply on a mailbox on behalf of someone else. In these cases, you can leverage the "Exchange Admin Shell" to configure an auto-reply to anyone who sends an email to the mailbox.
In the example below, replace `<username>` with the shortened username of the target user. (e.g. `nicole.rappe` not `nicole.rappe@bunny-lab.io`)
```powershell
Set-MailboxAutoReplyConfiguration -Identity <username> -AutoReplyState Scheduled -StartTime "1/1/2025 00:00:00" -EndTime "1/15/2025 00:00:00" -InternalMessage "Example,<br><br>Message here.<br><br>Thank you." -ExternalMessage "Example,<br><br>Message here.<br><br>Thank you."
```
!!! note "Internal vs External"
When you configure auto-replies, you can have different replies sent to people within the same organization versus external senders, keep this in mind based on the roles of the person.
!!! example "Example Email Reply"
The email auto reply will look something like this based on the command above.
```text
Example,
Message Here.
Thank you.
```
## Related Documentation
- [Related Email Documentation](<../../../../reference/Applications/Email/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,27 @@
---
tags:
- Microsoft Exchange
- Email
---
## Purpose
Start the Microsoft Exchange and supporting Windows services selected by this PowerShell script. The script sets their startup type to Automatic, so review the service requirements for the target server before running it.
!!! warning "Review Service Startup Changes"
The preserved script sets every selected service to Automatic and starts it, including services that may intentionally be disabled. It does not perform a service restart. Do not use it as the return-to-service step for Exchange SE; follow the baseline-specific service validation in the rolling-update workflow.
```powershell
$servicelist = Get-Service | Where-Object {$_.DisplayName -like "Microsoft Exchange *"}
$servicelist += Get-Service | Where-Object {$_.DisplayName -eq "IIS Admin Service"}
$servicelist += Get-Service | Where-Object { $_.DisplayName –eq "Windows Management Instrumentation" }
$servicelist += Get-Service | Where-Object { $_.DisplayName –eq "World Wide Web Publishing Service" }
foreach($service in $servicelist){
Set-Service $service -StartupType Automatic
Start-Service $service
}
```
## Related Documentation
- [Exchange SE Maintenance](<../../../../workflows/Applications/Email/Microsoft Exchange/Perform Exchange SE DAG Rolling Updates.md>) — Use its service-baseline validation when maintaining an SE DAG.
- [Related Email Documentation](<../../../../reference/Applications/Email/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -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.
@@ -0,0 +1,165 @@
---
tags:
- Minecraft
- PowerShell
- Scripting
---
## Purpose
This script was purpose-built for the homelab Minecraft servers in my homelab. It may need to be ported based on your own needs.
```powershell
clear
# #################################################
# # BUNNY LAB - MINECRAFT UPGRADE SCRIPT #
# #################################################
# Function to display the banner
function Show-Banner {
Write-Host "#################################################" -ForegroundColor Cyan
Write-Host "# BUNNY LAB - MINECRAFT UPGRADE SCRIPT #" -ForegroundColor Cyan
Write-Host "#################################################" -ForegroundColor Cyan
}
# Function to get user input for the zip file
function Get-ZipFileName {
Write-Host "Step 1: Getting the zip file name from user input..." -ForegroundColor Yellow
Write-Host "Please enter the name of the newest Minecraft ZIP file in the downloads folder (e.g., 'Server-Files-x.xx.zip'): " -ForegroundColor Yellow
$zipFileName = Read-Host
$zipFilePath = "C:\Users\nicole.rappe\Downloads\$zipFileName"
# Check if the zip file exists in the Downloads folder
Write-Host "Verifying if the specified ZIP file exists at: $zipFilePath" -ForegroundColor Yellow
if (-not (Test-Path $zipFilePath)) {
Write-Host "File not found! Please check the file name and try again." -ForegroundColor Red
exit
}
Write-Host "ZIP file found: $zipFilePath" -ForegroundColor Green
return $zipFilePath
}
# Function to unzip the file without nesting
function Unzip-ServerFiles {
param (
[string]$zipFilePath,
[string]$destinationFolder
)
Write-Host "Step 2: Unzipping the server files to: $destinationFolder" -ForegroundColor Yellow
# Create a temporary folder for extraction
$tempFolder = "$env:TEMP\MinecraftTemp"
Write-Host "Creating temporary folder for extraction at: $tempFolder" -ForegroundColor Yellow
# Remove the temporary folder if it exists, then recreate it
if (Test-Path $tempFolder) {
Write-Host "Temporary folder exists. Removing existing folder..." -ForegroundColor Yellow
Remove-Item -Recurse -Force $tempFolder
}
New-Item -ItemType Directory -Path $tempFolder
Write-Host "Unzipping new server files..." -ForegroundColor Green
Expand-Archive -Path $zipFilePath -DestinationPath $tempFolder -Force
Write-Host "Moving unzipped files to destination folder: $destinationFolder" -ForegroundColor Green
# Move the contents of the temporary folder to the destination
Get-ChildItem -Path $tempFolder -Recurse | Move-Item -Destination $destinationFolder -Force
# Clean up the temporary folder
Write-Host "Cleaning up temporary folder..." -ForegroundColor Yellow
Remove-Item -Recurse -Force $tempFolder
}
# Function to copy specific files/folders from the old deployment
function Copy-ServerData {
param (
[string]$sourceFolder,
[string]$destinationFolder
)
Write-Host "Step 3: Copying server data from: $sourceFolder to: $destinationFolder" -ForegroundColor Yellow
# Files to copy
Write-Host "Copying essential files..." -ForegroundColor Yellow
Copy-Item "$sourceFolder\eula.txt" "$destinationFolder\eula.txt" -Force
Copy-Item "$sourceFolder\user_jvm_args.txt" "$destinationFolder\user_jvm_args.txt" -Force
Copy-Item "$sourceFolder\ops.json" "$destinationFolder\ops.json" -Force
Copy-Item "$sourceFolder\server.properties" "$destinationFolder\server.properties" -Force
# Copy-Item "$sourceFolder\mods\ftbbackups2-neoforge-1.21-1.0.28.jar" "$destinationFolder\mods\ftbbackups2-neoforge-1.21-1.0.28.jar" -Force
Copy-Item "$sourceFolder\config\ftbbackups2.json" "$destinationFolder\config\ftbbackups2.json" -Force
Write-Host "Copying world data and backups folder..." -ForegroundColor Yellow
# Folder to copy (recursively)
Copy-Item "$sourceFolder\world" "$destinationFolder\world" -Recurse -Force
# New-Item -ItemType SymbolicLink -Path "\backups" -Target "Z:\"
}
# Function to rename the old folder with the current date
function Rename-OldServer {
param (
[string]$oldFolderPath
)
$currentDate = Get-Date -Format "MM-dd-yyyy"
$backupFolderPath = "$oldFolderPath.backup.$currentDate"
Write-Host "Step 4: Renaming old server folder to: $backupFolderPath" -ForegroundColor Yellow
Rename-Item -Path $oldFolderPath -NewName $backupFolderPath
Write-Host "Old server folder renamed to: $backupFolderPath" -ForegroundColor Green
}
# Function to rename the new deployment to 'ATM10'
function Rename-NewServer {
param (
[string]$newDeploymentPath,
[string]$finalServerPath
)
Write-Host "Step 5: Renaming new deployment folder to 'ATM10' at: $finalServerPath" -ForegroundColor Yellow
Rename-Item -Path $newDeploymentPath -NewName $finalServerPath
Write-Host "New server folder renamed to 'ATM10' at: $finalServerPath" -ForegroundColor Green
}
# Main Script Logic
# Show banner
Show-Banner
# Variables for folder paths
$oldServerFolder = "C:\Users\nicole.rappe\Desktop\Minecraft_Server\ATM10"
$newDeploymentFolder = "C:\Users\nicole.rappe\Desktop\Minecraft_Server\ATM10_NewDeployment"
$finalServerFolder = "C:\Users\nicole.rappe\Desktop\Minecraft_Server\ATM10"
# Step 1: Get the zip file name from the user
$zipFilePath = Get-ZipFileName
# Step 2: Unzip the file to the new deployment folder without nesting
Unzip-ServerFiles -zipFilePath $zipFilePath -destinationFolder $newDeploymentFolder
# Step 3: Copy necessary files/folders from the old server
Copy-ServerData -sourceFolder $oldServerFolder -destinationFolder $newDeploymentFolder
# Step 4: Rename the old server folder with the current date
Rename-OldServer -oldFolderPath $oldServerFolder
# Step 5: Rename the new deployment folder to 'ATM10'
Rename-NewServer -newDeploymentPath $newDeploymentFolder -finalServerPath $finalServerFolder
# Step 6. Create Symbolic Link to Backup Drive
Write-Host "Step 6: Create Symbolic Link to Backup Drive" -ForegroundColor Cyan
cd "C:\Users\nicole.rappe\Desktop\Minecraft_Server\ATM10"
cmd.exe /c mklink /D backups Z:\
# Step 7: Notify the user that the server is ready to launch
Write-Host "Step 7: Server Ready to Launch!" -ForegroundColor Cyan
Write-Host "Press any key to exit the script"
[System.Console]::ReadKey($true) # Waits for a key press and doesn't display the pressed key
clear
```
## Related Documentation
- [Related Applications Documentation](<../../../reference/Applications/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,37 @@
---
tags:
- Blue Iris
- Batch
- Monitoring
- Scripting
- Windows
---
## Purpose
This document records the procedure for blue iris server watchdog. Follow the environment assumptions and commands below.
```batch
@echo off
REM Change to the Blue Iris 5 directory
CD "C:\Program Files\Blue Iris 5"
:LOOP
REM Check if the BlueIrisAdmin.exe process is running
tasklist /FI "IMAGENAME eq BlueIris.exe" | find /I "BlueIris.exe" >nul
REM If the process is not found, start the process
if errorlevel 1 (
REM Start BlueIrisAdmin.exe
BlueIrisAdmin.exe
)
REM Wait for 10 seconds before checking again
timeout /t 10 /nobreak >nul
REM Go back to the beginning of the loop
GOTO :LOOP
```
## Related Documentation
- [Related Applications Documentation](<../../../reference/Applications/index.md>) — Find the connected deployments, procedures, and references for this subject.
+19
View File
@@ -0,0 +1,19 @@
---
tags:
- Applications
- Scripts
- Documentation
---
# Applications
## Purpose
Find scripts for applications. Follow the subject guide to choose the relevant environment and connect this material to the other document types.
## Includes
- Email
- Files and Collaboration
- Gaming and Media
- Home Automation
## Follow the Subject
[Applications](<../../reference/Applications/index.md>) explains the relationships and offers starting points for the documented tasks.