Skip to content

Instantly share code, notes, and snippets.

@earthdiver
Created June 9, 2025 14:34
Show Gist options
  • Select an option

  • Save earthdiver/c8cb00f56da743a5a247a8bf256430ac to your computer and use it in GitHub Desktop.

Select an option

Save earthdiver/c8cb00f56da743a5a247a8bf256430ac to your computer and use it in GitHub Desktop.
backup-files-on-update.bat
@PowerShell -NoP -W Hidden -C "$PSCP='%~f0';$PSSR='%~dp0'.TrimEnd('\');&([ScriptBlock]::Create((gc '%~f0'|?{$_.ReadCount -gt 1}|Out-String)))" & exit/b
# by earthdiver1
if ($PSCommandPath) {
$PSCP = $PSCommandPath
$PSSR = $PSScriptRoot
$code = '[DllImport("user32.dll")]public static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow);'
$type = Add-Type -MemberDefinition $code -Name Win32ShowWindowAsync -PassThru
[void]$type::ShowWindowAsync((Get-Process -PID $PID).MainWindowHandle,0) }
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
$menuItem = New-Object System.Windows.Forms.MenuItem "Exit"
$menuItem.add_Click({$notifyIcon.Visible=$False;while(-not $status.IsCompleted){Start-Sleep 1};$appContext.ExitThread()})
$contextMenu = New-Object System.Windows.Forms.ContextMenu
$contextMenu.MenuItems.AddRange($menuItem)
$notifyIcon = New-Object System.Windows.Forms.NotifyIcon
$notifyIcon.ContextMenu = $contextMenu
$notifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($PSCP)
$notifyIcon.Text = (Get-ChildItem $PSCP).BaseName
$notifyIcon.Visible = $True
$_syncHash = [hashtable]::Synchronized(@{})
$_syncHash.NI = $notifyIcon
$_syncHash.PSCP = $PSCP
$_syncHash.PSSR = $PSSR
$runspace = [RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$runspace.SessionStateProxy.SetVariable("_syncHash",$_syncHash)
$scriptBlock = Get-Content $PSCP | ?{ $on -or $_[1] -eq "!" }| %{ $on=1; $_ } | Out-String
$action=[ScriptBlock]::Create(@'
# param($Param1, $Param2)
Start-Transcript -LiteralPath ($_syncHash.PSCP -Replace '\..*?$',".log") -Append
Function Start-Sleep { [CmdletBinding(DefaultParameterSetName="S")]
param([parameter(Position=0,ParameterSetName="M")][Int]$Milliseconds,
[parameter(Position=0,ParameterSetName="S")][Int]$Seconds,[Switch]$NoExit)
if ($PsCmdlet.ParameterSetName -eq "S") {
$int = 5
for ($i = 0; $i -lt $Seconds; $i += $int) {
if (-not($NoExit -or $_syncHash.NI.Visible)) { exit }
Microsoft.PowerShell.Utility\Start-Sleep -Seconds $int }
} else {
$int = 100
for ($i = 0; $i -lt $Milliseconds; $i += $int) {
if (-not($NoExit -or $_syncHash.NI.Visible)) { exit }
Microsoft.PowerShell.Utility\Start-Sleep -Milliseconds $int }}}
$script:PSCommandPath = $_syncHash.PSCP
$script:PSScriptRoot = $_syncHash.PSSR
'@ + $scriptBlock)
$PS = [PowerShell]::Create().AddScript($action) #.AddArgument($Param1).AddArgument($Param2)
$PS.Runspace = $runspace
$status = $PS.BeginInvoke()
$appContext = New-Object System.Windows.Forms.ApplicationContext
[void][System.Windows.Forms.Application]::Run($appContext)
exit
#! ---------- ScriptBlock (Line No. 28) begins here ---------- DO NOT REMOVE THIS LINE
###################################################################################################################################
$src_root = "C:\somedir_containing_important_files"
$dst_root = "D:\backup"
$num_copy = 20
$interval_1 = 180
$interval_2 = 15
###################################################################################################################################
$ErrorActionPreference = "Stop"
while (-not (Test-Path $dst_root)) { Write-Output "$(Get-Date) Waiting..."; Start-Sleep 15 }
$MD5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
$src_root = Convert-Path -LiteralPath $src_root
$dst_root = Convert-Path -LiteralPath $dst_root
$old_mtime = New-Object 'System.Collections.Generic.Dictionary[String,String]'
$old_hash = New-Object 'System.Collections.Generic.Dictionary[String,String]'
$old_name = New-Object 'System.Collections.Generic.Dictionary[String,String]'
# Restore $old_hash of the last session from the backup.hash file.
if ( Test-Path -LiteralPath "$dst_root\backup.hash" ) {
try{
$sr = [IO.StreamReader]::new( "$dst_root\backup.hash", [Text.Encoding]::Default )
while ( -not $sr.EndOfStream ) {
$key,$value,$src_file = $sr.ReadLine() -split ","
$old_hash.$key = $value
$old_name.$key = $src_file
}
} catch {
throw
} finally {
$sr.Close()
}
# Regenerate backup.hash for filtering out the updated records.
try {
$sw = [IO.StreamWriter]::new( "$dst_root\backup.hash_", $false, [Text.Encoding]::Default )
$old_hash.GetEnumerator() | & { process{ $sw.WriteLine( "$($_.Key),$($_.Value),$($old_name.($_.Key))" ) } }
} catch {
throw
} finally {
$sw.Close()
}
if ( Test-Path "$dst_root\backup.hash_" ) {
Move-Item -LiteralPath "$dst_root\backup.hash_" -Destination "$dst_root\backup.hash" -Force
}
}
while ( $true ) {
try {
$sw = [IO.StreamWriter]::new( "$dst_root\backup.hash", $true, [Text.Encoding]::Default )
Get-ChildItem -LiteralPath $src_root -File -Recurse | ForEach-Object {
$file = $_
$src_file = $file.FullName
if ( $src_file -Like "$dst_root\*" ) { return }
# if ( $file.Length -gt 100MB ) { return }
$new_mtime = $file.LastWriteTime.ToString('yyyyMMddHHmm')
$idx = [Bitconverter]::ToString($MD5.ComputeHash([Text.Encoding]::Default.GetBytes( $src_file ))).Replace("-","")
# First, pick up candidates by file modification time.
if ( $new_mtime -ne $old_mtime.$idx ) {
$stream = try {
[IO.FileStream]::new( $src_file, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read )
} catch {
[IO.FileStream]::new( $src_file, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite )
}
if ( $stream ) {
try {
$new_hash = ( Get-FileHash -InputStream $stream -Algorithm MD5 ).Hash
} catch {
throw
} finally {
$stream.Close()
}
} else { throw }
# Then, use the hash value to determine the target.
if ( $new_hash -ne $old_hash.$idx ) {
$dst_dir = $dst_root + $file.DirectoryName.Substring($src_root.Length, $file.DirectoryName.Length - $src_root.Length)
if ( -not ( Test-Path -LiteralPath $dst_dir ) ) {
New-Item -ItemType Directory -Force -Path $dst_dir | Out-Null
}
$dst_file = Join-Path $dst_dir ($file.BaseName + "_" + $new_mtime + $file.Extension)
if ( -not ( Test-Path $dst_file ) ) {
Copy-Item -LiteralPath $src_file -Destination $dst_file -Force
$filter = $file.BaseName + "_????????????" + $file.Extension
Get-ChildItem -LiteralPath $dst_dir -File -Filter $filter | Sort-Object | Select-Object -SkipLast $num_copy | Remove-Item -Force
}
$old_hash.$idx = $new_hash
$sw.WriteLine( "$idx,$new_hash,$src_file" )
}
$old_mtime.$idx = $new_mtime
}
}
$sw.Close()
$interval = $interval_1
} catch [System.Exception] {
Write-Output $Error[0].ToString() $Error[0].InvocationInfo.PositionMessage
$interval = $interval_2
} finally {
if ( $sw -and $sw.BaseStream ) { $sw.Close() }
}
Start-Sleep $interval
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment