吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 885|回复: 6
收起左侧

[其他转载] hosts云端同步备份/恢复

  [复制链接]
pxhzai 发表于 2026-8-22 06:20
本帖最后由 pxhzai 于 2026-8-22 06:57 编辑

功能
1.增删查改 hosts 记录
2.本地/云端 多版本备份/恢复

3.可以设置备份上限,上限后自动删除hosts_前缀的备份,重命名的不删(不能hosts_前缀)。

使用方法  点启动.bat

云端备份
GitHub Token 打开 https://github.com/settings/tokens 点「Generate new token (classic)」 勾选 repo 权限(包含仓库读写)
Gitee Token 打开 https://gitee.com/profile/personal_access_tokens 点「生成新令牌」 勾选 projects 权限(仓库读写)


仓库地址https://github.com/pxhzaii/HostsCloud

下载地址https://pxhzai.lanzoub.com/i52lX43yjjrc





部分代码

[PowerShell] 纯文本查看 复制代码
# Hosts Manager - PowerShell backend
# Works on Win7+ with built-in PowerShell, no installation needed
# Features: CRUD hosts records, local multi-version backup, GitHub/Gitee cloud sync

$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

$AppDir    = Split-Path -Parent $MyInvocation.MyCommand.Path
$DataDir   = Join-Path $AppDir "data"
$BackupDir = Join-Path $DataDir "backups"
$ConfigFile = Join-Path $DataDir "config.json"
$HostsFile = Join-Path $env:SystemRoot "System32\drivers\etc\hosts"

# === Utility functions ===

function Ensure-Dirs {
    if (-not (Test-Path $DataDir))   { New-Item -ItemType Directory -Path $DataDir -Force | Out-Null }
    if (-not (Test-Path $BackupDir)) { New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null }
}

function Get-Timestamp { Get-Date -Format "yyyyMMdd_HHmmss" }

function ConvertTo-JsonArray($items) {
    if ($null -eq $items) { return "[]" }
    if ($items -is [System.Collections.IList] -or $items -is [System.Array] -or ($items -is [System.Collections.ArrayList])) {
        $parts = @()
        foreach ($item in $items) {
            $parts += ($item | ConvertTo-Json -Depth 10 -Compress)
        }
        return "[" + ($parts -join ",") + "]"
    }
    return $items | ConvertTo-Json -Depth 10 -Compress
}

function Send-Json($data) {
    # Fix: PowerShell ConvertTo-Json turns empty arrays into null and single-item arrays into objects
    # We handle the data field specially to ensure arrays stay as JSON arrays
    if ($data -is [System.Collections.Hashtable]) {
        $parts = @()
        foreach ($key in $data.Keys) {
            $val = $data[$key]
            if ($val -is [System.Collections.IList] -or $val -is [System.Array]) {
                $jsonVal = ConvertTo-JsonArray $val
            } elseif ($null -eq $val) {
                $jsonVal = "null"
            } elseif ($val -is [string]) {
                $jsonVal = '"' + ($val -replace '"', '\"' -replace '\\', '\\\\' -replace "`r", '\r' -replace "`n", '\n' -replace "`t", '\t') + '"'
            } elseif ($val -is [boolean]) {
                $jsonVal = if ($val) { "true" } else { "false" }
            } elseif ($val -is [int] -or $val -is [long] -or $val -is [double]) {
                $jsonVal = $val.ToString()
            } else {
                $jsonVal = $val | ConvertTo-Json -Depth 10 -Compress
            }
            $parts += '"' + $key + '":' + $jsonVal
        }
        $json = "{" + ($parts -join ",") + "}"
    } else {
        $json = $data | ConvertTo-Json -Depth 10 -Compress
        if ($null -eq $json) { $json = "null" }
    }
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
    $response.ContentType = "application/json; charset=utf-8"
    $response.ContentLength64 = $bytes.Length
    $response.OutputStream.Write($bytes, 0, $bytes.Length)
    $response.OutputStream.Close()
}

function Send-Ok($data, $msg) { Send-Json @{ ok = $true;  msg = $msg; data = $data } }
function Send-Fail($msg)     { Send-Json @{ ok = $false; msg = $msg; data = $null } }

function Send-Page($html) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($html)
    $response.ContentType = "text/html; charset=utf-8"
    $response.ContentLength64 = $bytes.Length
    $response.OutputStream.Write($bytes, 0, $bytes.Length)
    $response.OutputStream.Close()
}

# === Config ===

function Load-Config {
    Ensure-Dirs
    $default = @{ cloud = @{ type = ""; token = ""; repo = ""; branch = "main"; path = "hosts" }; maxBackups = 5 }
    if (Test-Path $ConfigFile) {
        try {
            $cfg = Get-Content $ConfigFile -Raw -Encoding UTF8 | ConvertFrom-Json
            if (-not $cfg.cloud) { $cfg | Add-Member -NotePropertyName cloud -NotePropertyValue $default.cloud }
            foreach ($k in $default.cloud.PSObject.Properties.Name) {
                if (-not ($cfg.cloud.PSObject.Properties.Name -contains $k)) {
                    $cfg.cloud | Add-Member -NotePropertyName $k -NotePropertyValue $default.cloud.$k
                }
            }
            if ($null -eq $cfg.maxBackups) { $cfg | Add-Member -NotePropertyName maxBackups -NotePropertyValue 5 }
            return $cfg
        } catch { }
    }
    return $default
}

function Save-Config($cfg) {
    Ensure-Dirs
    # Rebuild as plain hashtable to avoid PSCustomObject serialization artifacts
    $clean = @{ cloud = @{ type = $cfg.cloud.type; token = $cfg.cloud.token; repo = $cfg.cloud.repo; branch = $cfg.cloud.branch; path = $cfg.cloud.path }; maxBackups = $cfg.maxBackups }
    $clean | ConvertTo-Json -Depth 5 | Set-Content $ConfigFile -Encoding UTF8
}

# === Hosts read/write/parse ===

function Read-HostsText {
    if (-not (Test-Path $HostsFile)) { return "" }
    try { return Get-Content $HostsFile -Raw -Encoding UTF8 }
    catch { return $null }
}

function Write-HostsText($text) {
    $dir = Split-Path $HostsFile
    if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    [System.IO.File]::WriteAllText($HostsFile, $text, [System.Text.UTF8Encoding]::new($false))
}

function Parse-Hosts($text) {
    $records = [System.Collections.ArrayList]@()
    $lines = $text -split "`r?`n"
    for ($i = 0; $i -lt $lines.Length; $i++) {
        $line = $lines[$i]
        $stripped = $line.Trim()
        if (-not $stripped -or $stripped.StartsWith("#")) {
            [void]$records.Add(@{ ip = ""; domain = ""; comment = ""; enabled = $false; raw = $line; idx = $i })
            continue
        }
        $comment = ""
        $body = $stripped
        if ($stripped.Contains("#")) {
            $parts = $stripped -split "#", 2
            $body = $parts[0].Trim()
            $comment = $parts[1].Trim()
        }
        $fields = $body -split "\s+" | Where-Object { $_ -ne "" }
        if ($fields.Length -ge 2) {
            [void]$records.Add(@{ ip = $fields[0]; domain = $fields[1]; comment = $comment; enabled = $true; raw = $line; idx = $i })
        } else {
            [void]$records.Add(@{ ip = ""; domain = ""; comment = ""; enabled = $true; raw = $line; idx = $i })
        }
    }
    , $records
}

function Serialize-Records($records) {
    $lines = @()
    foreach ($r in $records) {
        if (-not $r.enabled) {
            $lines += if ($r.raw -ne $null) { $r.raw } else { "" }
        } else {
            $ip = ($r.ip + "").Trim()
            $domain = ($r.domain + "").Trim()
            $comment = ($r.comment + "").Trim()
            if ($ip -and $domain) {
                $line = "$ip $domain"
                if ($comment) { $line += "  # $comment" }
                $lines += $line
            } else {
                $lines += if ($r.raw -ne $null) { $r.raw } else { "" }
            }
        }
    }
    return ($lines -join "`n")
}

function Get-Records {
    $text = Read-HostsText
    if ($text -eq $null) { return $null }
    return Parse-Hosts $text
}

function Save-Records($records) {
    $text = Serialize-Records $records
    Write-HostsText $text
}

# === Local backup ===

function Backup-Current {
    Ensure-Dirs
    $text = Read-HostsText
    if ($text -eq $null) { return $false }
    $ts = Get-Timestamp
    $path = Join-Path $BackupDir "hosts_$ts.txt"
    [System.IO.File]::WriteAllText($path, $text, [System.Text.UTF8Encoding]::new($false))

    # Keep only the latest N backups (configurable, default 5), delete older ones
    $maxBk = (Load-Config).maxBackups
    if (-not $maxBk -or $maxBk -lt 1) { $maxBk = 5 }
    $oldFiles = Get-ChildItem $BackupDir -Filter "hosts_*.txt" -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -Skip $maxBk
    if ($oldFiles) {
        foreach ($f in $oldFiles) { Remove-Item $f.FullName -Force -ErrorAction SilentlyContinue }
    }
    return $path
}

function List-Backups {
    Ensure-Dirs
    $items = [System.Collections.ArrayList]@()
    Get-ChildItem $BackupDir -Filter "*.txt" -ErrorAction SilentlyContinue | Sort-Object Name -Descending | ForEach-Object {
        $isNamed = -not $_.Name.StartsWith("hosts_")
        [void]$items.Add(@{ name = $_.Name; time = $_.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"); size = $_.Length; named = $isNamed })
    }
    , $items
}

function Rename-Backup($oldName, $newName) {
    $safeOld = [System.IO.Path]::GetFileName($oldName)
    $safeNew = [System.IO.Path]::GetFileName($newName)
    if (-not $safeNew.EndsWith(".txt")) { $safeNew = $safeNew + ".txt" }
    $oldPath = Join-Path $BackupDir $safeOld
    $newPath = Join-Path $BackupDir $safeNew
    if (-not (Test-Path $oldPath)) { return $false, "Backup not found" }
    if (Test-Path $newPath) { return $false, "Name already exists" }
    Rename-Item -Path $oldPath -NewName $safeNew -Force
    return $true, $safeNew
}

function Read-Backup($name) {
    $safe = [System.IO.Path]::GetFileName($name)
    $path = Join-Path $BackupDir $safe
    if (-not (Test-Path $path)) { return $null }
    return [System.IO.File]::ReadAllText($path, [System.Text.UTF8Encoding]::new($false))
}

function Delete-Backup($name) {
    $safe = [System.IO.Path]::GetFileName($name)
    $path = Join-Path $BackupDir $safe
    if (Test-Path $path) { Remove-Item $path -Force; return $true }
    return $false
}

function Restore-Backup($name) {
    $content = Read-Backup $name
    if ($content -eq $null) { return $false }
    Backup-Current | Out-Null
    Write-HostsText $content
    return $true
}

# === Cloud sync (GitHub / Gitee REST API) ===

function Get-CloudConfig { (Load-Config).cloud }

function Get-EncodedPath($path) {
    return ($path -split "/" | ForEach-Object { [uri]::EscapeDataString($_) }) -join "/"
}

function Test-Cloud {
    $c = Get-CloudConfig
    $type = $c.type; $token = $c.token; $repo = $c.repo
    if ($type -eq "github") {
        try {
            $r = Invoke-RestMethod -Uri "https://api.github.com/user" -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -TimeoutSec 15
            return @{ ok = $true; msg = "GitHub: $($r.login)" }
        } catch { return @{ ok = $false; msg = "GitHub: $($_.Exception.Message)" } }
    } elseif ($type -eq "gitee") {
        try {
            $r = Invoke-RestMethod -Uri "https://gitee.com/api/v5/user?access_token=$([uri]::EscapeDataString($token))" -TimeoutSec 15
            return @{ ok = $true; msg = "Gitee: $($r.login)" }
        } catch { return @{ ok = $false; msg = "Gitee: $($_.Exception.Message)" } }
    }
    return @{ ok = $false; msg = "Please select cloud type" }
}

function Cloud-GetFile {
    $c = Get-CloudConfig
    $type = $c.type; $token = $c.token
    $repo  = ($c.repo).Trim().Trim("/")
    $branch = if ($c.branch) { $c.branch } else { "main" }
    $path = if ($c.path) { $c.path } else { "hosts" }
    if (-not $repo) { return $null, "Repo not configured" }

    if ($type -eq "github") {
        try {
            $url = "https://raw.githubusercontent.com/$repo/$([uri]::EscapeDataString($branch))/$(Get-EncodedPath $path)"
            $content = (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 15).Content
            return $content, $null
        } catch { }
        try {
            $url = "https://api.github.com/repos/$repo/contents/$(Get-EncodedPath $path)?ref=$([uri]::EscapeDataString($branch))"
            $r = Invoke-RestMethod -Uri $url -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -TimeoutSec 15
            $content = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($r.content))
            return $content, $null
        } catch { return $null, "Read failed: $($_.Exception.Message)" }
    } elseif ($type -eq "gitee") {
        try {
            $url = "https://gitee.com/$repo/raw/$([uri]::EscapeDataString($branch))/$(Get-EncodedPath $path)"
            $content = (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 15).Content
            return $content, $null
        } catch { }
        try {
            $url = "https://gitee.com/api/v5/repos/$repo/contents/$(Get-EncodedPath $path)?ref=$([uri]::EscapeDataString($branch))&access_token=$([uri]::EscapeDataString($token))"
            $r = Invoke-RestMethod -Uri $url -TimeoutSec 15
            $content = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($r.content))
            return $content, $null
        } catch { return $null, "Read failed: $($_.Exception.Message)" }
    }
    return $null, "Please select cloud type"
}

function Cloud-UpdateFile($content, $message) {
    $c = Get-CloudConfig
    $type = $c.type; $token = $c.token
    $repo  = ($c.repo).Trim().Trim("/")
    $branch = if ($c.branch) { $c.branch } else { "main" }
    if (-not $repo) { return $false, "Repo not configured" }
    $encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($content))

    # Always create a new timestamped file in backups/ directory
    $ts = Get-Timestamp
    $cloudPath = "backups/hosts_$ts.txt"
    $encodedPath = Get-EncodedPath $cloudPath

    if ($type -eq "github") {
        $headers = @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" }
        $payload = @{ message = $message; content = $encoded; branch = $branch }
        $json = $payload | ConvertTo-Json -Depth 5
        try {
            $url = "https://api.github.com/repos/$repo/contents/$encodedPath"
            Invoke-RestMethod -Uri $url -Method PUT -Headers $headers -Body $json -ContentType "application/json" -TimeoutSec 15 | Out-Null
            return $true, "Pushed to GitHub"
        } catch { return $false, "Push failed: $($_.Exception.Message)" }
    } elseif ($type -eq "gitee") {
        $payload = @{ access_token = $token; content = $encoded; message = $message; branch = $branch }
        $json = $payload | ConvertTo-Json -Depth 5
        try {
            $url = "https://gitee.com/api/v5/repos/$repo/contents/$encodedPath"
            Invoke-RestMethod -Uri $url -Method POST -Body $json -ContentType "application/json" -TimeoutSec 15 | Out-Null
            return $true, "Pushed to Gitee"
        } catch { return $false, "Push failed: $($_.Exception.Message)" }
    }
    return $false, "Please select cloud type"
}

function Cloud-ListHistory {
    $c = Get-CloudConfig
    $type = $c.type; $token = $c.token
    $repo  = ($c.repo).Trim().Trim("/")
    $branch = if ($c.branch) { $c.branch } else { "main" }
    if (-not $repo) { , @(); return }
    $items = [System.Collections.ArrayList]@()
    $encodedPath = Get-EncodedPath "backups"

    if ($type -eq "github") {
        try {
            $url = "https://api.github.com/repos/$repo/contents/$encodedPath?ref=$([uri]::EscapeDataString($branch))"
            $r = Invoke-RestMethod -Uri $url -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -TimeoutSec 15
            $files = @($r | Where-Object { $_.name -like "*.txt" } | Sort-Object name -Descending)
            foreach ($f in $files) {
                $isNamed = -not $f.name.StartsWith("hosts_")
                [void]$items.Add(@{ name = $f.name; path = $f.path; size = $f.size; named = $isNamed })
            }
        } catch { Write-Host "GH history error: $_" }
    } elseif ($type -eq "gitee") {
        try {
            $url = "https://gitee.com/api/v5/repos/$repo/contents/$encodedPath`?ref=$([uri]::EscapeDataString($branch))&access_token=$([uri]::EscapeDataString($token))"
            $r = Invoke-RestMethod -Uri $url -TimeoutSec 15
            $files = @($r | Where-Object { $_.name -like "*.txt" } | Sort-Object name -Descending)
            foreach ($f in $files) {
                $isNamed = -not $f.name.StartsWith("hosts_")
                [void]$items.Add(@{ name = $f.name; path = $f.path; size = $f.size; named = $isNamed })
            }
        } catch { Write-Host "Gitee history error: $_" }
    }
    , $items
}

function Cloud-ReadBackup($filePath) {
    $c = Get-CloudConfig
    $type = $c.type; $token = $c.token
    $repo  = ($c.repo).Trim().Trim("/")
    $branch = if ($c.branch) { $c.branch } else { "main" }
    $encodedPath = Get-EncodedPath $filePath

    if ($type -eq "github") {
        try {
            $url = "https://raw.githubusercontent.com/$repo/$([uri]::EscapeDataString($branch))/$encodedPath"
            return (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 15).Content, $null
        } catch { }
        try {
            $url = "https://api.github.com/repos/$repo/contents/$encodedPath?ref=$([uri]::EscapeDataString($branch))"
            $r = Invoke-RestMethod -Uri $url -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -TimeoutSec 15
            return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($r.content)), $null
        } catch { return $null, "Read failed: $($_.Exception.Message)" }
    } elseif ($type -eq "gitee") {
        try {
            $url = "https://gitee.com/$repo/raw/$([uri]::EscapeDataString($branch))/$encodedPath"
            return (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 15).Content, $null
        } catch { }
        try {
            $url = "https://gitee.com/api/v5/repos/$repo/contents/$encodedPath?ref=$([uri]::EscapeDataString($branch))&access_token=$([uri]::EscapeDataString($token))"
            $r = Invoke-RestMethod -Uri $url -TimeoutSec 15
            return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($r.content)), $null
        } catch { return $null, "Read failed: $($_.Exception.Message)" }
    }
    return $null, "Please select cloud type"
}

function Cloud-DeleteBackup($filePath) {
    $c = Get-CloudConfig
    $type = $c.type; $token = $c.token
    $repo  = ($c.repo).Trim().Trim("/")
    $branch = if ($c.branch) { $c.branch } else { "main" }
    $encodedPath = Get-EncodedPath $filePath

    if ($type -eq "github") {
        try {
            $url = "https://api.github.com/repos/$repo/contents/$encodedPath?ref=$([uri]::EscapeDataString($branch))"
            $r = Invoke-RestMethod -Uri $url -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -TimeoutSec 15
            $sha = $r.sha
            $delUrl = "https://api.github.com/repos/$repo/contents/$encodedPath"
            $delBody = @{ message = "delete $filePath"; sha = $sha; branch = $branch } | ConvertTo-Json -Compress
            Invoke-RestMethod -Uri $delUrl -Method DELETE -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -Body $delBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            return $true, "Deleted"
        } catch { return $false, "Delete failed: $($_.Exception.Message)" }
    } elseif ($type -eq "gitee") {
        try {
            $url = "https://gitee.com/api/v5/repos/$repo/contents/$encodedPath`?ref=$([uri]::EscapeDataString($branch))&access_token=$([uri]::EscapeDataString($token))"
            $r = Invoke-RestMethod -Uri $url -TimeoutSec 15
            $sha = $r.sha
            $delUrl = "https://gitee.com/api/v5/repos/$repo/contents/$encodedPath"
            $delBody = @{ access_token = $token; sha = $sha; message = "delete $filePath"; branch = $branch } | ConvertTo-Json -Compress
            Invoke-RestMethod -Uri $delUrl -Method DELETE -Body $delBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            return $true, "Deleted"
        } catch { return $false, "Delete failed: $($_.Exception.Message)" }
    }
    return $false, "Please select cloud type"
}

function Cloud-RenameBackup($oldPath, $newName) {
    $c = Get-CloudConfig
    $type = $c.type; $token = $c.token
    $repo  = ($c.repo).Trim().Trim("/")
    $branch = if ($c.branch) { $c.branch } else { "main" }
    if (-not $repo) { return $false, "Repo not configured" }
    if (-not $newName.EndsWith(".txt")) { $newName = $newName + ".txt" }
    $newPath = "backups/$newName"
    $encodedOld = Get-EncodedPath $oldPath
    $encodedNew = Get-EncodedPath $newPath

    if ($type -eq "github") {
        $headers = @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" }
        try {
            $url = "https://api.github.com/repos/$repo/contents/$encodedOld`?ref=$([uri]::EscapeDataString($branch))"
            $r = Invoke-RestMethod -Uri $url -Headers $headers -TimeoutSec 15
            $content = $r.content
            $sha = $r.sha
            $createBody = @{ message = "rename to $newName"; content = $content; branch = $branch } | ConvertTo-Json -Compress
            $createUrl = "https://api.github.com/repos/$repo/contents/$encodedNew"
            Invoke-RestMethod -Uri $createUrl -Method PUT -Headers $headers -Body $createBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            $delBody = @{ message = "remove old $oldPath"; sha = $sha; branch = $branch } | ConvertTo-Json -Compress
            $delUrl = "https://api.github.com/repos/$repo/contents/$encodedOld"
            Invoke-RestMethod -Uri $delUrl -Method DELETE -Headers $headers -Body $delBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            return $true, $newName
        } catch { return $false, "Rename failed: $($_.Exception.Message)" }
    } elseif ($type -eq "gitee") {
        try {
            $url = "https://gitee.com/api/v5/repos/$repo/contents/$encodedOld`?ref=$([uri]::EscapeDataString($branch))&access_token=$([uri]::EscapeDataString($token))"
            $r = Invoke-RestMethod -Uri $url -TimeoutSec 15
            $content = $r.content
            $sha = $r.sha
            $createBody = @{ access_token = $token; content = $content; message = "rename to $newName"; branch = $branch } | ConvertTo-Json -Compress
            $createUrl = "https://gitee.com/api/v5/repos/$repo/contents/$encodedNew"
            Invoke-RestMethod -Uri $createUrl -Method POST -Body $createBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            $delBody = @{ access_token = $token; sha = $sha; message = "remove old $oldPath"; branch = $branch } | ConvertTo-Json -Compress
            $delUrl = "https://gitee.com/api/v5/repos/$repo/contents/$encodedOld"
            Invoke-RestMethod -Uri $delUrl -Method DELETE -Body $delBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            return $true, $newName
        } catch { return $false, "Rename failed: $($_.Exception.Message)" }
    }
    return $false, "Please select cloud type"
}

function Cloud-CleanupOldBackups {
    $cfg = Load-Config
    $maxBk = $cfg.maxBackups
    if (-not $maxBk -or $maxBk -lt 1) { $maxBk = 5 }
    $c = $cfg.cloud
    $type = $c.type; $token = $c.token
    $repo  = ($c.repo).Trim().Trim("/")
    $branch = if ($c.branch) { $c.branch } else { "main" }
    if (-not $repo) { return }
    $encodedPath = Get-EncodedPath "backups"

    if ($type -eq "github") {
        try {
            $url = "https://api.github.com/repos/$repo/contents/$encodedPath?ref=$([uri]::EscapeDataString($branch))"
            $r = Invoke-RestMethod -Uri $url -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -TimeoutSec 15
            $files = @($r | Where-Object { $_.name -like "hosts_*.txt" } | Sort-Object name -Descending | Select-Object -Skip $maxBk)
            foreach ($f in $files) {
                $delUrl = "https://api.github.com/repos/$repo/contents/$($f.path)"
                $delBody = @{ message = "cleanup $($f.name)"; sha = $f.sha; branch = $branch } | ConvertTo-Json -Compress
                Invoke-RestMethod -Uri $delUrl -Method DELETE -Headers @{ Authorization = "Bearer $token"; Accept = "application/vnd.github+json" } -Body $delBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            }
        } catch { Write-Host "GH cleanup error: $_" }
    } elseif ($type -eq "gitee") {
        try {
            $url = "https://gitee.com/api/v5/repos/$repo/contents/$encodedPath`?ref=$([uri]::EscapeDataString($branch))&access_token=$([uri]::EscapeDataString($token))"
            $r = Invoke-RestMethod -Uri $url -TimeoutSec 15
            $files = @($r | Where-Object { $_.name -like "hosts_*.txt" } | Sort-Object name -Descending | Select-Object -Skip $maxBk)
            foreach ($f in $files) {
                $infoUrl = "https://gitee.com/api/v5/repos/$repo/contents/$(Get-EncodedPath $f.path)`?ref=$([uri]::EscapeDataString($branch))&access_token=$([uri]::EscapeDataString($token))"
                $info = Invoke-RestMethod -Uri $infoUrl -TimeoutSec 15
                $delUrl = "https://gitee.com/api/v5/repos/$repo/contents/$(Get-EncodedPath $f.path)"
                $delBody = @{ access_token = $token; sha = $info.sha; message = "cleanup $($f.name)"; branch = $branch } | ConvertTo-Json -Compress
                Invoke-RestMethod -Uri $delUrl -Method DELETE -Body $delBody -ContentType "application/json" -TimeoutSec 15 | Out-Null
            }
        } catch { Write-Host "Gitee cleanup error: $_" }
    }
}

# === HTTP server ===

function Read-RequestBody {
    $len = $request.ContentLength64
    if ($len -le 0) { return @{} }
    $reader = New-Object System.IO.StreamReader($request.InputStream, [System.Text.Encoding]::UTF8)
    $body = $reader.ReadToEnd()
    try { return $body | ConvertFrom-Json } catch { return @{} }
}

function Parse-Query($rawUrl) {
    $q = @{}
    if ($rawUrl.Contains("?")) {
        $qs = $rawUrl.Split("?", 2)[1]
        foreach ($pair in $qs.Split("&")) {
            $kv = $pair.Split("=", 2)
            $q[$kv[0]] = [System.Uri]::UnescapeDataString($kv[1])
        }
    }
    return $q
}

function Start-Server {
    param($Port = 8686)
    $htmlFile = Join-Path $AppDir "index.html"
    $html = ""
    if (Test-Path $htmlFile) { $html = [System.IO.File]::ReadAllText($htmlFile, [System.Text.UTF8Encoding]::new($false)) }
    if (-not $html) { $html = "<html><body><h1>Hosts Manager</h1><p>Missing index.html</p></body></html>" }

    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add("http://127.0.0.1:$Port/")
    $listener.Start()

    Write-Host "========================================================"
    Write-Host "  Hosts Manager Started"
    Write-Host "  URL: [url=http://127.0.0.1:]http://127.0.0.1:[/url]$Port"
    Write-Host "  Press Ctrl+C to exit"
    Write-Host "========================================================"

    try {
        while ($listener.IsListening) {
            $ctx = $listener.GetContext()
            $script:request  = $ctx.Request
            $script:response = $ctx.Response
            $url = $ctx.Request.Url.AbsolutePath
            $method = $ctx.Request.HttpMethod
            try {
                if ($method -eq "GET") {
                    switch ($url) {
                        "/" { Send-Page $html }
                        "/index.html" { Send-Page $html }
                        "/api/config" {
                            $cfg = Load-Config
                            $maxBk = $cfg.maxBackups
                            if (-not $maxBk -or $maxBk -lt 1) { $maxBk = 5 }
                            $pub = @{ type = $cfg.cloud.type; repo = $cfg.cloud.repo; branch = $cfg.cloud.branch; path = $cfg.cloud.path; has_token = [bool]$cfg.cloud.token; maxBackups = $maxBk }
                            Send-Ok $pub ""
                        }
                        "/api/records" {
                            $records = Get-Records
                            if ($records -eq $null) { Send-Fail "No permission to read hosts, run as admin" }
                            else { Send-Ok $records "" }
                        }
                        "/api/backups" { Send-Ok (List-Backups) "" }
                        "/api/backup/content" {
                            $q = Parse-Query $ctx.Request.Url.AbsoluteUri
                            $content = Read-Backup $q.name
                            if ($content -eq $null) { Send-Fail "Backup not found" }
                            else { Send-Ok $content "" }
                        }
                        "/api/cloud/history" { Send-Ok (Cloud-ListHistory) "" }
                        "/api/cloud/content" {
                            $q = Parse-Query $ctx.Request.Url.AbsoluteUri
                            $result = Cloud-ReadBackup $q.path
                            if ($result[1]) { Send-Fail $result[1] }
                            else { Send-Ok $result[0] "" }
                        }
                        default { $ctx.Response.StatusCode = 404; $ctx.Response.Close() }
                    }
                } elseif ($method -eq "POST") {
                    $data = Read-RequestBody
                    switch ($url) {
                        "/api/records/save" {
                            try { Save-Records $data.records; Send-Ok $true "Saved" }
                            catch { Send-Fail "Save failed: $($_.Exception.Message)" }
                        }
                        "/api/open-hosts" {
                            try {
                                Start-Process "notepad.exe" -ArgumentList "`"$HostsFile`""
                                Send-Ok $true "Opening hosts file"
                            } catch { Send-Fail "Open failed: $($_.Exception.Message)" }
                        }
                        "/api/backup/create" {
                            $r = Backup-Current
                            if ($r) { Send-Ok $r "Backup created" } else { Send-Fail "Backup failed" }
                        }
                        "/api/backup/restore" {
                            try { Restore-Backup $data.name | Out-Null; Send-Ok $true "Restored" }
                            catch { Send-Fail "Restore failed: $($_.Exception.Message)" }
                        }
                        "/api/backup/delete" {
                            Delete-Backup $data.name | Out-Null
                            Send-Ok $true "Deleted"
                        }
                        "/api/backup/rename" {
                            $r = Rename-Backup $data.oldName $data.newName
                            if ($r[0]) { Send-Ok $r[1] "Renamed" } else { Send-Fail $r[1] }
                        }
                        "/api/config/save" {
                            $cfg = Load-Config
                            foreach ($k in @("type", "repo", "branch", "path")) {
                                if ($data.cloud.$k -ne $null) { $cfg.cloud.$k = ($data.cloud.$k + "").Trim() }
                            }
                            if ($data.cloud.token) { $cfg.cloud.token = ($data.cloud.token + "").Trim() }
                            if ($data.maxBackups -ne $null) {
                                $mb = [int]($data.maxBackups)
                                if ($mb -ge 1 -and $mb -le 100) { $cfg.maxBackups = $mb }
                            }
                            Save-Config $cfg
                            Send-Ok $true "Config saved"
                        }
                        "/api/cloud/test" {
                            $r = Test-Cloud
                            if ($r.ok) { Send-Ok $r.msg $r.msg } else { Send-Fail $r.msg }
                        }
                        "/api/cloud/push" {
                            $text = Read-HostsText
                            if ($text -eq $null) { Send-Fail "Cannot read hosts"; break }
                            $result = Cloud-UpdateFile $text "backup hosts $(Get-Timestamp)"
                            if ($result[0]) {
                                Cloud-CleanupOldBackups
                                Send-Ok $true $result[1]
                            } else { Send-Fail $result[1] }
                        }
                        "/api/cloud/pull" {
                            $result = Cloud-GetFile
                            if ($result[1]) { Send-Fail $result[1] } else { Send-Ok $result[0] "" }
                        }
                        "/api/cloud/restore" {
                            $filePath = $data.path
                            if ($filePath) { $result = Cloud-ReadBackup $filePath } else { $result = Cloud-GetFile }
                            if ($result[1]) { Send-Fail $result[1]; break }
                            try {
                                Backup-Current | Out-Null
                                Write-HostsText $result[0]
                                Send-Ok $true "Restored from cloud"
                            } catch { Send-Fail "Write failed: $($_.Exception.Message)" }
                        }
                        "/api/cloud/delete" {
                            $filePath = $data.path
                            if (-not $filePath) { Send-Fail "Path is required" }
                            else {
                                $result = Cloud-DeleteBackup $filePath
                                if ($result[0]) { Send-Ok $true $result[1] } else { Send-Fail $result[1] }
                            }
                        }
                        "/api/cloud/rename" {
                            $oldPath = $data.path
                            $newName = $data.newName
                            if (-not $oldPath -or -not $newName) { Send-Fail "Path and newName are required" }
                            else {
                                $result = Cloud-RenameBackup $oldPath $newName
                                if ($result[0]) { Send-Ok $result[1] "Renamed" } else { Send-Fail $result[1] }
                            }
                        }
                        default { $ctx.Response.StatusCode = 404; $ctx.Response.Close() }
                    }
                }
            } catch {
                try { Send-Fail $_.Exception.Message } catch { }
            }
        }
    } finally {
        $listener.Stop()
    }
}

Start-Server -Port 8686

免费评分

参与人数 5吾爱币 +9 热心值 +4 收起 理由
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
c3ecbb8d + 1 谢谢@Thanks!
ot1686 + 1 谢谢@Thanks!
weipohai + 1 我很赞同!
1588 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

samson12138 发表于 2026-8-22 10:03
挺实用的
daixiangjiang 发表于 2026-8-22 10:54
ot1686 发表于 2026-8-22 11:17
有介面...方便编辑....不用那么麻烦再开notepad编辑了....谢谢大佬...^^
dujiu3611 发表于 2026-8-22 12:20
只能备份 github 吗?很多时候不代理的时候,这个都是慢的,甚至无法访问,应该多加个其他备份选项。
不过挺好的了,支持原创,感谢分享。
zzs9511 发表于 2026-8-23 09:54
没用过这种来,我来试试
sddedlfa84 发表于 2026-8-24 14:33
老哥好,你那个验证码识别3.0离线电脑能用吗,能发我一份吗
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-12 08:22

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表