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]

Download RAW File