1
# PowerShell Console App for GitHub File Mirroring via Task Scheduler
param(
[switch]$RunMirror
)
function List-GitHubMirrorTasks {
Get-ScheduledTask | Where-Object { $_.Description -like "*GitHubMirrorApp*" }
}
function Parse-TaskAction {
param($action)
if ($action -match "-Uri '(.*?)'.*-OutFile '(.*?)'") {
return @{ URL = $matches[1]; Path = $matches[2] }
} else {
return @{ URL = ""; Path = "" }
}
}
function Extract-TaskInfo {
param($task)
$urlPath = @{ URL = ""; Path = "" }
if ($task.Description -match "GitHubMirrorApp — (.*?) => (.*)") {
$urlPath = @{ URL = $matches[1]; Path = $matches[2] }
}
return $urlPath
}
function Create-SmartDownloadArgs {
param($url, $path)
return "-NoProfile -WindowStyle Hidden -Command `"`$url = '$url'; `$file = '$path'; `$req = [System.Net.HttpWebRequest]::Create(`$url); `$req.Method = 'GET'; if (Test-Path `$file) { `$req.IfModifiedSince = (Get-Item `$file).LastWriteTime }; try { `$res = `$req.GetResponse(); [System.IO.File]::WriteAllBytes(`$file, (New-Object System.IO.BinaryReader(`$res.GetResponseStream())).ReadBytes(`$res.ContentLength)); `$res.Close(); Write-Host 'File downloaded successfully.' } catch [System.Net.WebException] { if (`$_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotModified) { Write-Host 'File has not been modified since the last download.' } else { Write-Host 'An error occurred: ' + `$_.Exception.Message } }`""
}
function Create-TaskComponents {
param($url, $path, $username)
$psArgs = Create-SmartDownloadArgs -url $url -path $path
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $psArgs
$trigger = New-ScheduledTaskTrigger -Daily -At 5am
if ([string]::IsNullOrWhiteSpace($username)) {
$username = "$env:USERDOMAIN\$env:USERNAME"
}
$principal = New-ScheduledTaskPrincipal -UserId $username -LogonType S4U -RunLevel Highest
$description = "GitHubMirrorApp — $url => $path"
return @{
Action = $action
Trigger = $trigger
Principal = $principal
Description = $description
}
}
function Mirror-Files {
$tasks = List-GitHubMirrorTasks
foreach ($task in $tasks) {
$parsed = Parse-TaskAction $task.Actions.Arguments
try {
Invoke-WebRequest -Uri $parsed.URL -OutFile $parsed.Path
Write-Host "Updated: $($parsed.Path)"
} catch {
Write-Warning "Failed to update $($parsed.URL): $_"
}
}
}
if ($RunMirror) {
Mirror-Files
exit
}
function Show-Menu {
Clear-Host
Write-Host "Currently Registered GitHubMirror Tasks:" -ForegroundColor Yellow
$i = 0
List-GitHubMirrorTasks | ForEach-Object {
$desc = $_.Description
# Extract URL and path from description rather than action arguments
$urlPath = Extract-TaskInfo $_
# Check if task is disabled and set color accordingly
$taskColor = [System.ConsoleColor]::White
$stateInfo = ""
if ($_.State -eq "Disabled") {
$taskColor = [System.ConsoleColor]::Red
$stateInfo = " [DISABLED]"
}
# Simplified display - all tasks run daily at 5 AM
Write-Host "[$i] $($_.TaskName)$stateInfo" -ForegroundColor $taskColor
Write-Host " => $($urlPath.URL)" -ForegroundColor $taskColor
$i++
}
Write-Host ""
Write-Host "GitHub File Mirror Tool" -ForegroundColor Cyan
Write-Host "[A] Add Entry" -ForegroundColor Green
Write-Host "[E] Edit Entry (press a number first, then E)" -ForegroundColor Green
Write-Host "[C] Copy Entry (press a number first, then C)" -ForegroundColor Green
Write-Host "[D] Delete Entry (press a number first, then D)" -ForegroundColor Green
Write-Host "[T] Toggle Enable/Disable (press a number first, then T)" -ForegroundColor Green
Write-Host "[B] Create Batch File (press a number first, then B)" -ForegroundColor Green
Write-Host "[Q] Quit" -ForegroundColor Green
Write-Host ""
Write-Host "Tip: Type a task number followed by an action key (e.g. '0B' to create a batch file for task 0)" -ForegroundColor Cyan
Write-Host ""
Write-Host "Press a key to select an option..." -ForegroundColor Yellow
}
function Get-TaskAction {
$numberBuffer = ""
$waitingForAction = $false
while ($true) {
$key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
$keyChar = $key.Character
# Handle numeric keys
if ($keyChar -ge '0' -and $keyChar -le '9') {
$numberBuffer += $keyChar
Write-Host $keyChar -NoNewline
$waitingForAction = $true
continue
}
# Handle action keys
$action = $keyChar.ToString().ToUpper()
# If we have a number in the buffer and an action key
if ($waitingForAction -and $numberBuffer -ne "" -and $action -match '[ECDTB]') {
Write-Host $action
$index = [int]$numberBuffer
switch ($action) {
'E' { Edit-Entry -Index $index }
'C' { Copy-Entry -Index $index }
'D' { Delete-Entry -Index $index }
'T' { Toggle-Active -Index $index }
'B' { Create-BatchFile -Index $index }
}
return $true
}
# Handle single action keys
elseif ($action -eq 'A') {
Write-Host "A"
Add-Entry
return $true
}
elseif ($action -eq 'Q') {
Write-Host "Q"
return $false
}
elseif ($action -eq 'C' -and $numberBuffer -eq "") {
Write-Host "C"
$index = Read-Host "Enter the number of the task to copy"
if ($index -match '^\d+$') {
Copy-Entry -Index ([int]$index)
} else {
Write-Host "Invalid index. Please enter a number." -ForegroundColor Red
}
return $true
}
elseif ($action -eq 'B' -and $numberBuffer -eq "") {
Write-Host "B"
$index = Read-Host "Enter the number of the task to create a batch file for"
if ($index -match '^\d+$') {
Create-BatchFile -Index ([int]$index)
} else {
Write-Host "Invalid index. Please enter a number." -ForegroundColor Red
}
return $true
}
# Clear buffer on invalid input
$numberBuffer = ""
$waitingForAction = $false
}
}
function Get-CommonEntryInfo {
param(
[string]$ExistingURL = "",
[string]$ExistingPath = ""
)
$url = Read-Host "Enter GitHub URL (leave blank to keep current: $ExistingURL)"
if ([string]::IsNullOrWhiteSpace($url)) { $url = $ExistingURL }
if ($url -match "github.com/(.+)/blob/(.+)") {
$url = "https://raw.githubusercontent.com/$($matches[1])/$($matches[2])"
Write-Host "Auto-converted to raw GitHub URL: $url" -ForegroundColor Green
}
try {
$response = Invoke-WebRequest -Uri $url -Method Head -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -ne 200) {
Write-Warning "URL is not accessible (Status: $($response.StatusCode)). Aborting."
return $null
}
} catch {
Write-Warning "Failed to reach URL: $_.Exception.Message"
return $null
}
# Parse existing path into directory and filename if provided
$existingDirectory = ""
$existingFileName = ""
if (-not [string]::IsNullOrWhiteSpace($ExistingPath)) {
$existingDirectory = Split-Path -Parent $ExistingPath
$existingFileName = Split-Path -Leaf $ExistingPath
}
# Get new filename or use existing/default
$defaultFileName = [System.Web.HttpUtility]::UrlDecode(($url -split '/')[-1])
if (-not [string]::IsNullOrWhiteSpace($existingFileName)) {
$promptFileName = $existingFileName
} else {
$promptFileName = $defaultFileName
}
$fileName = Read-Host "Enter file name (default: $promptFileName)"
if ([string]::IsNullOrWhiteSpace($fileName)) { $fileName = $promptFileName }
# Get directory or use existing
$directoryPrompt = if (-not [string]::IsNullOrWhiteSpace($existingDirectory)) {
"Enter full output directory path (default: $existingDirectory)"
} else {
"Enter full output directory path"
}
$directory = Read-Host $directoryPrompt
if ([string]::IsNullOrWhiteSpace($directory) -and -not [string]::IsNullOrWhiteSpace($existingDirectory)) {
$directory = $existingDirectory
}
if (-not (Test-Path $directory)) {
Write-Host "Invalid directory. Aborting."
return $null
}
$path = Join-Path $directory $fileName
return @{ URL = $url; Path = $path }
}
function Add-Entry {
$info = Get-CommonEntryInfo
if ($null -eq $info) { return }
$url = $info.URL
$path = $info.Path
$hash = [System.BitConverter]::ToString((New-Object Security.Cryptography.SHA256Managed).ComputeHash([Text.Encoding]::UTF8.GetBytes($url))).Replace("-", "").Substring(0, 8)
$taskName = "GitHubMirror_$hash"
$username = Read-Host "Enter username to run task as (leave blank to use current user)"
$useCurrentUser = [string]::IsNullOrWhiteSpace($username)
if ($useCurrentUser) {
$username = "$env:USERDOMAIN\$env:USERNAME"
}
$taskComponents = Create-TaskComponents -url $url -path $path -username $username
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}
if ($useCurrentUser) {
Register-ScheduledTask -TaskName $taskName -Action $taskComponents.Action -Trigger $taskComponents.Trigger -Principal $taskComponents.Principal -Description $taskComponents.Description
} else {
$plainPassword = Read-Host "Enter password (visible)"
Register-ScheduledTask -TaskName $taskName -Action $taskComponents.Action -Trigger $taskComponents.Trigger -User $username -Password $plainPassword -Description $taskComponents.Description -RunLevel Highest
}
Write-Host "Task '$taskName' created."
}
function Edit-Entry {
param([int]$Index)
$task = (List-GitHubMirrorTasks)[$Index]
if ($null -eq $task) { Write-Host "Invalid index."; return }
# Extract URL and path from description
$urlPath = Extract-TaskInfo $task
$url = $urlPath.URL
$path = $urlPath.Path
$info = Get-CommonEntryInfo -ExistingURL $url -ExistingPath $path
if ($null -eq $info) { return }
$url = $info.URL
$path = $info.Path
Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false
$taskComponents = Create-TaskComponents -url $url -path $path -username ""
Register-ScheduledTask -TaskName $task.TaskName -Action $taskComponents.Action -Trigger $taskComponents.Trigger -Principal $taskComponents.Principal -Description $taskComponents.Description
Write-Host "Task '$($task.TaskName)' updated."
}
function Delete-Entry {
param([int]$Index)
$task = (List-GitHubMirrorTasks)[$Index]
if ($null -eq $task) { Write-Host "Invalid index."; return }
Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false
Write-Host "Task '$($task.TaskName)' deleted."
}
function Toggle-Active {
param([int]$Index)
$task = (List-GitHubMirrorTasks)[$Index]
if ($null -eq $task) { Write-Host "Invalid index."; return }
if ($task.State -eq 'Ready') {
Disable-ScheduledTask -TaskName $task.TaskName
Write-Host "Task '$($task.TaskName)' disabled."
} else {
Enable-ScheduledTask -TaskName $task.TaskName
Write-Host "Task '$($task.TaskName)' enabled."
}
}
function Copy-Entry {
param([int]$Index)
$task = (List-GitHubMirrorTasks)[$Index]
if ($null -eq $task) { Write-Host "Invalid index."; return }
# Extract URL and path from description
$urlPath = Extract-TaskInfo $task
$url = $urlPath.URL
$path = $urlPath.Path
Write-Host "Creating a new task based on task #$Index" -ForegroundColor Cyan
Write-Host "You can modify the URL, path, and other settings as needed" -ForegroundColor Cyan
# Use existing values as defaults but allow changes
$info = Get-CommonEntryInfo -ExistingURL $url -ExistingPath $path
if ($null -eq $info) { return }
$url = $info.URL
$path = $info.Path
# Generate a new hash/task name with timestamp to ensure uniqueness
$timestamp = Get-Date -Format "yyMMddHHmmss"
$hash = [System.BitConverter]::ToString((New-Object Security.Cryptography.SHA256Managed).ComputeHash([Text.Encoding]::UTF8.GetBytes("$url$timestamp"))).Replace("-", "").Substring(0, 8)
$taskName = "GitHubMirror_$hash"
# Get user credentials
$username = Read-Host "Enter username to run task as (leave blank to use current user)"
$useCurrentUser = [string]::IsNullOrWhiteSpace($username)
if ($useCurrentUser) {
$username = "$env:USERDOMAIN\$env:USERNAME"
}
$taskComponents = Create-TaskComponents -url $url -path $path -username $username
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}
if ($useCurrentUser) {
Register-ScheduledTask -TaskName $taskName -Action $taskComponents.Action -Trigger $taskComponents.Trigger -Principal $taskComponents.Principal -Description $taskComponents.Description
} else {
$plainPassword = Read-Host "Enter password (visible)"
Register-ScheduledTask -TaskName $taskName -Action $taskComponents.Action -Trigger $taskComponents.Trigger -User $username -Password $plainPassword -Description $taskComponents.Description -RunLevel Highest
}
Write-Host "New task '$taskName' created based on existing task." -ForegroundColor Green
}
function Generate-BatchFileContent {
param($taskName, $url, $path)
$scriptContent = @"
# PowerShell script to create a scheduled task for GitHub file mirroring
`$url = '$url'
`$path = '$path'
# Create directory if it doesn't exist
`$directory = Split-Path -Parent `$path
if (-not (Test-Path `$directory)) {
Write-Host "Creating directory: `$directory"
New-Item -ItemType Directory -Path `$directory | Out-Null
}
# Download file immediately
Write-Host "Downloading file..."
`$req = [System.Net.HttpWebRequest]::Create(`$url)
`$req.Method = "GET"
try {
`$res = `$req.GetResponse()
[System.IO.File]::WriteAllBytes(`$path, (New-Object System.IO.BinaryReader(`$res.GetResponseStream())).ReadBytes(`$res.ContentLength))
`$res.Close()
Write-Host "File downloaded successfully to `$path"
} catch {
Write-Host "Error downloading file: `$_"
}
# Create the scheduled task
`$taskName = "$taskName"
`$psArgs = "-NoProfile -WindowStyle Hidden -Command ```"`$url = '`$url'; ```$file = '`$path'; ```$req = [System.Net.HttpWebRequest]::Create(```$url); ```$req.Method = 'GET'; if (Test-Path ```$file) { ```$req.IfModifiedSince = (Get-Item ```$file).LastWriteTime }; try { ```$res = ```$req.GetResponse(); [System.IO.File]::WriteAllBytes(```$file, (New-Object System.IO.BinaryReader(```$res.GetResponseStream())).ReadBytes(```$res.ContentLength)); ```$res.Close(); Write-Host 'File downloaded successfully.' } catch [System.Net.WebException] { if (```$_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotModified) { Write-Host 'File has not been modified since the last download.' } else { Write-Host 'An error occurred: ' + ```$_.Exception.Message } }```""
`$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument `$psArgs
`$trigger = New-ScheduledTaskTrigger -Daily -At 5am
`$principal = New-ScheduledTaskPrincipal -UserId "`$env:USERDOMAIN\`$env:USERNAME" -LogonType S4U -RunLevel Highest
`$description = "GitHubMirrorApp — `$url => `$path"
# Remove existing task if it exists
if (Get-ScheduledTask -TaskName `$taskName -ErrorAction SilentlyContinue) {
Write-Host "Removing existing task: `$taskName"
Unregister-ScheduledTask -TaskName `$taskName -Confirm:`$false
}
# Register the task
Write-Host "Creating scheduled task: `$taskName"
Register-ScheduledTask -TaskName `$taskName -Action `$action -Trigger `$trigger -Principal `$principal -Description `$description
Write-Host "Task created successfully!"
"@
$batchContent = @"
@echo off
:: Self-elevate if not already running as administrator
NET FILE 1>NUL 2>NUL
if '%errorlevel%' == '0' goto :already_admin
echo Requesting administrative privileges...
powershell -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
exit /b
:already_admin
echo Creating scheduled task for GitHub file mirroring...
powershell.exe -ExecutionPolicy Bypass -File "%~dp0TaskScript_$($taskName.Replace("GitHubMirror_", "")).ps1"
echo.
echo Task setup complete!
pause
"@
return @{
ScriptContent = $scriptContent
BatchContent = $batchContent
}
}
function Create-BatchFile {
param([int]$Index)
$task = (List-GitHubMirrorTasks)[$Index]
if ($null -eq $task) { Write-Host "Invalid index."; return }
# Extract URL and path from description
$urlPath = Extract-TaskInfo $task
$url = $urlPath.URL
$path = $urlPath.Path
if ([string]::IsNullOrWhiteSpace($url) -or [string]::IsNullOrWhiteSpace($path)) {
Write-Host "Could not extract URL and path from task." -ForegroundColor Red
return
}
# Create a directory for batch files if it doesn't exist
$batchDir = Join-Path $PSScriptRoot "TaskBatches"
if (-not (Test-Path $batchDir)) {
New-Item -ItemType Directory -Path $batchDir | Out-Null
}
# Create a name for the batch file based on the task name
$taskShortName = $task.TaskName.Replace("GitHubMirror_", "")
$batchFile = Join-Path $batchDir "Install_$taskShortName.bat"
# Create a PowerShell script file with the task creation logic
$psFile = Join-Path $batchDir "TaskScript_$taskShortName.ps1"
$fileContents = Generate-BatchFileContent -taskName $task.TaskName -url $url -path $path
# Write the PowerShell script with UTF-8 encoding with BOM to ensure proper encoding
[System.IO.File]::WriteAllText($psFile, $fileContents.ScriptContent, [System.Text.Encoding]::UTF8)
# Write the batch file with ASCII encoding
[System.IO.File]::WriteAllText($batchFile, $fileContents.BatchContent, [System.Text.Encoding]::ASCII)
Write-Host "Batch file created: $batchFile" -ForegroundColor Green
Write-Host "PowerShell script created: $psFile" -ForegroundColor Green
# Ask if user wants to open the batch file's folder
$openFolder = Read-Host "Do you want to open the folder containing the batch file? (Y/N)"
if ($openFolder -eq "Y" -or $openFolder -eq "y") {
Start-Process "explorer.exe" -ArgumentList $batchDir
}
}
# Main loop
while ($true) {
Show-Menu
$continue = Get-TaskAction
if (-not $continue) {
break
}
Write-Host "Press any key to return to menu..."
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
For immediate assistance, please email our customer support: [email protected]