Last active
January 11, 2026 16:17
-
-
Save superyngo/f74f4749882df654dfdf286b7f718a9e to your computer and use it in GitHub Desktop.
Manage rclone mount through ssh config.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| rclone Mount 管理工具 (PowerShell 版本) | |
| .DESCRIPTION | |
| 功能: mount, unmount, status, show, help | |
| 使用 SSH config 設定掛載 rclone SFTP remote 到本地目錄 | |
| .PARAMETER Operation | |
| 操作類型: mount/mnt, unmount/umount/umnt, status, show, help | |
| .PARAMETER RemoteSpec | |
| SSH config 中的 Host 名稱,格式: <remote_name>[:<remote_path>] | |
| 或使用 --all/--a 進行批量操作 | |
| .PARAMETER CustomMountPoint | |
| 自定義本地掛載點 (選填) | |
| .EXAMPLE | |
| .\rclonemm.ps1 mount mom | |
| 掛載 mom 根目錄到預設位置 | |
| .EXAMPLE | |
| .\rclonemm.ps1 mount mom:/WEB/logs | |
| 掛載 mom 指定目錄到預設位置 | |
| .EXAMPLE | |
| .\rclonemm.ps1 mount mom D:\custom\path | |
| 掛載 mom 根目錄到自定義位置 | |
| .EXAMPLE | |
| .\rclonemm.ps1 mount --all | |
| 批量掛載所有 SSH Host | |
| .EXAMPLE | |
| .\rclonemm.ps1 unmount mom | |
| 卸載 mom | |
| .EXAMPLE | |
| .\rclonemm.ps1 status mom | |
| 檢查 mom 狀態 | |
| .NOTES | |
| 版本: 1.0.0 | |
| 日期: 2026-01-02 | |
| 作者: Translated from Bash version by GitHub Copilot | |
| 原始 Bash 版本更新: 2025-12-19 | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Position = 0)] | |
| [string]$Operation, | |
| [Parameter(Position = 1)] | |
| [string]$RemoteSpec, | |
| [Parameter(Position = 2)] | |
| [string]$CustomMountPoint | |
| ) | |
| # ==================== 錯誤處理設定 ==================== | |
| $ErrorActionPreference = 'Stop' | |
| Set-StrictMode -Version Latest | |
| # ==================== 全域設定 ==================== | |
| $script:Config = @{ | |
| SSHConfig = "$env:USERPROFILE\.ssh\config" | |
| MountBaseDir = "C:\mnt" | |
| LogDir = "$env:LOCALAPPDATA\rclonemm\logs" | |
| LogRetentionDays = 7 | |
| MountTimeout = 10 | |
| AccessTestTimeout = 10 | |
| } | |
| # 支援自定義 PREFIX 環境變數 | |
| if ($env:RCLONEMM_PREFIX) { | |
| Write-Verbose "使用自定義 PREFIX: $env:RCLONEMM_PREFIX" | |
| $script:Config.MountBaseDir = "$env:RCLONEMM_PREFIX\mnt" | |
| $script:Config.LogDir = "$env:RCLONEMM_PREFIX\var\log" | |
| } | |
| # 日誌檔案路徑 | |
| $script:LogFile = Join-Path $script:Config.LogDir "rclone_mount_$(Get-Date -Format 'yyyyMMdd').log" | |
| # ==================== 輔助函數: 日誌與訊息 ==================== | |
| <# | |
| .SYNOPSIS | |
| 輸出帶顏色的訊息並記錄到日誌 | |
| #> | |
| function Write-ColorMessage { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')] | |
| [string]$Level, | |
| [Parameter(Mandatory)] | |
| [string]$Message | |
| ) | |
| $color = switch ($Level) { | |
| 'INFO' { 'Green' } | |
| 'WARN' { 'Yellow' } | |
| 'ERROR' { 'Red' } | |
| 'DEBUG' { 'Cyan' } | |
| } | |
| Write-Host "[$Level] $Message" -ForegroundColor $color | |
| # 記錄到日誌檔案 | |
| Write-Log -Level $Level -Message $Message | |
| } | |
| <# | |
| .SYNOPSIS | |
| 寫入日誌檔案 | |
| #> | |
| function Write-Log { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$Level, | |
| [Parameter(Mandatory)] | |
| [string]$Message | |
| ) | |
| try { | |
| $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' | |
| $logEntry = "[$timestamp] [$Level] $Message" | |
| # 確保日誌目錄存在 | |
| $logDir = Split-Path $script:LogFile -Parent | |
| if (-not (Test-Path $logDir)) { | |
| New-Item -Path $logDir -ItemType Directory -Force | Out-Null | |
| } | |
| # 寫入日誌 | |
| Add-Content -Path $script:LogFile -Value $logEntry -ErrorAction SilentlyContinue | |
| } catch { | |
| # 忽略日誌寫入錯誤 | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 清理過期日誌檔案 | |
| #> | |
| function Clear-OldLogs { | |
| [CmdletBinding()] | |
| param() | |
| try { | |
| $logDir = $script:Config.LogDir | |
| if (-not (Test-Path $logDir)) { | |
| return | |
| } | |
| $cutoffDate = (Get-Date).AddDays(-$script:Config.LogRetentionDays) | |
| Get-ChildItem -Path $logDir -Filter "rclone_mount_*.log" | | |
| Where-Object { $_.LastWriteTime -lt $cutoffDate } | | |
| Remove-Item -Force -ErrorAction SilentlyContinue | |
| } catch { | |
| # 忽略清理錯誤 | |
| } | |
| } | |
| # ==================== 輔助函數: 依賴檢查 ==================== | |
| <# | |
| .SYNOPSIS | |
| 檢查必要的依賴套件 | |
| #> | |
| function Test-Dependencies { | |
| [CmdletBinding()] | |
| param() | |
| $missingDeps = @() | |
| # 檢查 rclone | |
| if (-not (Get-Command rclone -ErrorAction SilentlyContinue)) { | |
| $missingDeps += "rclone" | |
| } | |
| # 檢查 WinFsp (透過註冊表或檔案系統) | |
| $winfspPaths = @( | |
| "$env:ProgramFiles\WinFsp", | |
| "${env:ProgramFiles(x86)}\WinFsp" | |
| ) | |
| $winfspInstalled = $winfspPaths | Where-Object { Test-Path $_ } | |
| if (-not $winfspInstalled) { | |
| $missingDeps += "WinFsp" | |
| } | |
| # 如果有缺失的依賴,顯示錯誤並退出 | |
| if ($missingDeps.Count -gt 0) { | |
| Write-ColorMessage -Level ERROR -Message "缺少必要的依賴套件:" | |
| foreach ($dep in $missingDeps) { | |
| Write-Host " ✗ $dep" -ForegroundColor Red | |
| } | |
| Write-Host "" | |
| Write-Host "請安裝缺少的套件:" -ForegroundColor Yellow | |
| Write-Host " rclone: https://rclone.org/downloads/" -ForegroundColor Yellow | |
| Write-Host " WinFsp: https://winfsp.dev/" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 檢查是否具有管理員權限 | |
| #> | |
| function Test-AdminPrivileges { | |
| [CmdletBinding()] | |
| param() | |
| $identity = [Security.Principal.WindowsIdentity]::GetCurrent() | |
| $principal = [Security.Principal.WindowsPrincipal]$identity | |
| return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | |
| } | |
| # ==================== 輔助函數: SSH Config 解析 ==================== | |
| <# | |
| .SYNOPSIS | |
| 解析 SSH config 獲取指定 Host 的指定屬性 | |
| #> | |
| function Get-SSHConfigValue { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter(Mandatory)] | |
| [string]$Key | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return $null | |
| } | |
| $content = Get-Content $sshConfigPath -Raw | |
| $lines = $content -split "`r?`n" | |
| $inHostBlock = $false | |
| foreach ($line in $lines) { | |
| # 檢查是否進入目標 Host 區塊 | |
| if ($line -match '^\s*Host\s+(.+)$') { | |
| $hostPatterns = $Matches[1] -split '\s+' | |
| $inHostBlock = $hostPatterns -contains $HostAlias | |
| continue | |
| } | |
| # 如果在目標 Host 區塊內,查找屬性 | |
| if ($inHostBlock) { | |
| if ($line -match "^\s*$Key\s+(.+)$") { | |
| return $Matches[1].Trim() | |
| } | |
| # 遇到下一個 Host 區塊,停止搜尋 | |
| if ($line -match '^\s*Host\s+') { | |
| break | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| <# | |
| .SYNOPSIS | |
| 獲取 SSH Host 的完整設定 | |
| #> | |
| function Get-SSHHostConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $hostname = Get-SSHConfigValue -HostAlias $HostAlias -Key 'HostName' | |
| $user = Get-SSHConfigValue -HostAlias $HostAlias -Key 'User' | |
| $port = Get-SSHConfigValue -HostAlias $HostAlias -Key 'Port' | |
| $identityFile = Get-SSHConfigValue -HostAlias $HostAlias -Key 'IdentityFile' | |
| $proxyJump = Get-SSHConfigValue -HostAlias $HostAlias -Key 'ProxyJump' | |
| # 如果沒有明確的 HostName,使用 Host alias | |
| if (-not $hostname) { | |
| $hostname = $HostAlias | |
| } | |
| # 展開 ~ 為實際路徑 | |
| if ($identityFile -and $identityFile.StartsWith('~')) { | |
| $identityFile = $identityFile -replace '^~', $env:USERPROFILE | |
| } | |
| return [PSCustomObject]@{ | |
| HostAlias = $HostAlias | |
| HostName = $hostname | |
| User = $user | |
| Port = $port | |
| IdentityFile = $identityFile | |
| ProxyJump = $proxyJump | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 組建 --sftp-ssh 選項 | |
| #> | |
| function Get-SftpSshOption { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [PSCustomObject]$Config | |
| ) | |
| $sshCmd = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=2" | |
| # 添加 Port | |
| if ($Config.Port) { | |
| $sshCmd += " -p $($Config.Port)" | |
| } | |
| # 添加 IdentityFile | |
| if ($Config.IdentityFile) { | |
| $sshCmd += " -i `"$($Config.IdentityFile)`"" | |
| } | |
| # 添加 ProxyJump | |
| if ($Config.ProxyJump) { | |
| $sshCmd += " -J $($Config.ProxyJump)" | |
| } | |
| # 組建完整的 SSH 連線字串 user@hostname | |
| $sshTarget = if ($Config.User) { | |
| "$($Config.User)@$($Config.HostName)" | |
| } else { | |
| $Config.HostName | |
| } | |
| # 返回完整的 SSH 命令(不含外層引號,因為會作為參數傳遞) | |
| return "$sshCmd $sshTarget" | |
| } | |
| # ==================== 輔助函數: 掛載點管理 ==================== | |
| <# | |
| .SYNOPSIS | |
| 檢查指定路徑是否已掛載 | |
| #> | |
| function Test-Mounted { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint | |
| ) | |
| # 透過 Get-Process 查找 rclone 進程並檢查命令列 | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| return $false | |
| } | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| if ($cmdLine -like "*mount*" -and $cmdLine -like "*$MountPoint*") { | |
| return $true | |
| } | |
| } catch { | |
| # 忽略權限錯誤 | |
| } | |
| } | |
| return $false | |
| } | |
| <# | |
| .SYNOPSIS | |
| 確保掛載點的父目錄存在,但掛載點本身必須不存在 | |
| Windows 下 rclone 要求掛載點不能是已存在的目錄 | |
| #> | |
| function Ensure-MountPoint { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint | |
| ) | |
| # 檢查掛載點是否已存在 | |
| if (Test-Path $MountPoint) { | |
| Write-ColorMessage -Level ERROR -Message "掛載點已存在: $MountPoint (Windows 下 rclone 要求掛載點必須不存在)" | |
| return $false | |
| } | |
| # 檢查父目錄是否存在 | |
| $parentPath = Split-Path $MountPoint -Parent | |
| if (-not $parentPath) { | |
| Write-ColorMessage -Level ERROR -Message "無效的掛載點路徑: $MountPoint" | |
| return $false | |
| } | |
| if (-not (Test-Path $parentPath)) { | |
| Write-ColorMessage -Level INFO -Message "建立父目錄: $parentPath" | |
| try { | |
| New-Item -Path $parentPath -ItemType Directory -Force | Out-Null | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "無法建立父目錄: $parentPath - $_" | |
| return $false | |
| } | |
| } | |
| Write-ColorMessage -Level INFO -Message "掛載點路徑驗證通過: $MountPoint" | |
| return $true | |
| } | |
| <# | |
| .SYNOPSIS | |
| 查找指定 host alias 的所有掛載點 | |
| 返回格式: @{RemotePath=''; MountPoint=''} | |
| #> | |
| function Find-MountPoints { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $mountPoints = @() | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| return ,$mountPoints | |
| } | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| # 檢查是否為 mount 操作 | |
| if ($cmdLine -notlike "*mount*") { | |
| continue | |
| } | |
| # 解析命令列,提取 remote source 和 mount point | |
| # 格式: rclone mount "remote:path" "mount_point" [options] | |
| # 或: rclone mount remote:path mount_point [options] | |
| $pattern = 'mount\s+"?([^":]+):([^"]*)"?\s+"?([^"]+)"?' | |
| if ($cmdLine -match $pattern) { | |
| $remote = $Matches[1] | |
| $remotePath = $Matches[2] | |
| $mountPoint = $Matches[3] | |
| if ($remote -eq $HostAlias) { | |
| $mountPoints += [PSCustomObject]@{ | |
| RemotePath = $remotePath | |
| MountPoint = $mountPoint | |
| ProcessId = $process.Id | |
| } | |
| } | |
| } | |
| } catch { | |
| # 忽略錯誤 | |
| } | |
| } | |
| return ,$mountPoints | |
| } | |
| <# | |
| .SYNOPSIS | |
| 查找匹配 host alias 和掛載點的 rclone 進程 PID | |
| #> | |
| function Find-RclonePids { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$MountPoint | |
| ) | |
| $pids = @() | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| return ,$pids | |
| } | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| # 檢查是否匹配 host alias | |
| if ($cmdLine -notlike "*$HostAlias*") { | |
| continue | |
| } | |
| # 如果指定了掛載點,還要匹配掛載點 | |
| if ($MountPoint -and $cmdLine -notlike "*$MountPoint*") { | |
| continue | |
| } | |
| $pids += $process.Id | |
| } catch { | |
| # 忽略錯誤 | |
| } | |
| } | |
| return ,$pids | |
| } | |
| # ==================== 核心函數: 掛載操作 ==================== | |
| <# | |
| .SYNOPSIS | |
| 核心掛載函數(單一 host 的掛載邏輯) | |
| #> | |
| function Invoke-MountSingle { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$RemotePath = '', | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint | |
| ) | |
| # 取得 SSH Host 設定 | |
| $sshConfig = Get-SSHHostConfig -HostAlias $HostAlias | |
| # 組建 --sftp-ssh 選項 | |
| $sftpSshOption = Get-SftpSshOption -Config $sshConfig | |
| # 組合遠端來源路徑 | |
| $remoteSource = if ($RemotePath) { | |
| "${HostAlias}:$RemotePath" | |
| } else { | |
| "${HostAlias}:" | |
| } | |
| # 檢查目標掛載點是否被占用 | |
| if (Test-Mounted -MountPoint $MountPoint) { | |
| Write-ColorMessage -Level ERROR -Message "掛載點 $MountPoint 已被其他檔案系統占用" | |
| return $false | |
| } | |
| # 確保掛載點路徑有效(父目錄存在,但掛載點本身不存在) | |
| if (-not (Ensure-MountPoint -MountPoint $MountPoint)) { | |
| return $false | |
| } | |
| # 在掛載前先建立 rclone config | |
| Write-ColorMessage -Level INFO -Message "建立 rclone config: $HostAlias" | |
| try { | |
| $configResult = & rclone config create $HostAlias sftp 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $configResult" | |
| return $false | |
| } | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $_" | |
| return $false | |
| } | |
| # 建構 rclone 參數 | |
| $argString = "mount `"$remoteSource`" `"$MountPoint`" --vfs-cache-mode full --default-permissions --multi-thread-streams 4 --multi-thread-cutoff 100M --low-level-retries 2 --sftp-disable-hashcheck --sftp-ssh `"$sftpSshOption`"" | |
| Write-ColorMessage -Level INFO -Message "掛載指令: rclone $argString" | |
| Write-ColorMessage -Level INFO -Message "提示: 如果 SSH 需要密碼,請在提示時輸入" | |
| # 執行掛載命令(直接在背景執行) | |
| try { | |
| # 使用 Start-Process 並直接傳遞參數 | |
| # 將整個命令字串傳遞,讓 PowerShell 自動處理引號 | |
| $process = Start-Process -FilePath "rclone" -ArgumentList $argString -NoNewWindow -PassThru | |
| # 等待掛載完成 | |
| $count = 0 | |
| while ($count -lt $script:Config.MountTimeout) { | |
| Write-ColorMessage -Level INFO -Message "等待掛載中... ($($count + 1)/$($script:Config.MountTimeout))" | |
| Start-Sleep -Seconds 1 | |
| if (Test-Mounted -MountPoint $MountPoint) { | |
| Write-ColorMessage -Level INFO -Message "掛載進程已啟動: ${HostAlias}:$RemotePath -> $MountPoint" | |
| # 等待 rclone 服務完全啟動 | |
| # 檢測掛載點是否可訪問,重試幾次 | |
| Write-ColorMessage -Level INFO -Message "等待 rclone 服務啟動完成..." | |
| $accessTestCount = 0 | |
| $maxAccessTests = 5 | |
| $accessSuccess = $false | |
| while ($accessTestCount -lt $maxAccessTests) { | |
| Start-Sleep -Seconds 2 | |
| $accessTestCount++ | |
| Write-ColorMessage -Level INFO -Message "測試掛載點訪問... (嘗試 $accessTestCount/$maxAccessTests)" | |
| try { | |
| # 嘗試訪問掛載點 | |
| if (Test-Path $MountPoint) { | |
| $testResult = Get-ChildItem -Path $MountPoint -ErrorAction Stop | Select-Object -First 1 | |
| Write-ColorMessage -Level INFO -Message "✓ 掛載點測試通過,日誌: $($script:LogFile)" | |
| $accessSuccess = $true | |
| break | |
| } | |
| } catch { | |
| Write-ColorMessage -Level DEBUG -Message "訪問測試失敗 (嘗試 $accessTestCount/$maxAccessTests): $_" | |
| # 繼續重試 | |
| } | |
| } | |
| if ($accessSuccess) { | |
| # 掛載成功後清除 rclone config | |
| Write-ColorMessage -Level INFO -Message "清除 rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $true | |
| } else { | |
| Write-ColorMessage -Level ERROR -Message "✗ 掛載點在 $maxAccessTests 次嘗試後仍無法訪問,掛載失敗" | |
| Write-ColorMessage -Level WARN -Message "執行清理操作..." | |
| # 執行清理 | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias -MountPoint $MountPoint | |
| foreach ($procId in $rclonePids) { | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| # 清除 rclone config | |
| Write-ColorMessage -Level INFO -Message "清除 rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $false | |
| } | |
| } | |
| $count++ | |
| } | |
| Write-ColorMessage -Level ERROR -Message "掛載超時,請檢查日誌: $($script:LogFile)" | |
| Write-ColorMessage -Level WARN -Message "執行清理操作..." | |
| # 執行清理 | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias -MountPoint $MountPoint | |
| foreach ($procId in $rclonePids) { | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| # 清除 rclone config | |
| Write-ColorMessage -Level INFO -Message "清除 rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $false | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "rclone mount 命令執行失敗: $_" | |
| Write-ColorMessage -Level WARN -Message "執行清理操作..." | |
| # 清除 rclone config | |
| Write-ColorMessage -Level INFO -Message "清除 rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $false | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 掛載 remote(用戶接口函數) | |
| #> | |
| function Mount-RcloneRemote { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$RemotePath = '', | |
| [Parameter()] | |
| [string]$CustomMountPoint = '' | |
| ) | |
| # 決定掛載點:自定義 > 預設 | |
| $mountPoint = if ($CustomMountPoint) { | |
| $CustomMountPoint | |
| } else { | |
| Join-Path $script:Config.MountBaseDir $HostAlias | |
| } | |
| Write-ColorMessage -Level INFO -Message "使用 --sftp-ssh 選項進行掛載" | |
| Write-ColorMessage -Level INFO -Message "掛載 ${HostAlias}:$RemotePath 到 $mountPoint" | |
| if ($CustomMountPoint) { | |
| Write-ColorMessage -Level INFO -Message "使用自定義掛載點: $CustomMountPoint" | |
| } | |
| # 調用核心掛載函數 | |
| return Invoke-MountSingle -HostAlias $HostAlias -RemotePath $RemotePath -MountPoint $mountPoint | |
| } | |
| # ==================== 核心函數: 卸載操作 ==================== | |
| <# | |
| .SYNOPSIS | |
| 執行實際的卸載操作 | |
| #> | |
| function Invoke-Unmount { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint, | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| Write-ColorMessage -Level INFO -Message "卸載 $MountPoint" | |
| # 查找相關的 rclone 進程 | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias -MountPoint $MountPoint | |
| if ($rclonePids.Count -eq 0) { | |
| Write-ColorMessage -Level WARN -Message "找不到相關的 rclone 進程" | |
| return $false | |
| } | |
| Write-ColorMessage -Level INFO -Message "發現相關的 rclone 進程: $($rclonePids -join ', ')" | |
| Write-ColorMessage -Level INFO -Message "正在終止進程..." | |
| # 先嘗試正常終止 | |
| foreach ($procId in $rclonePids) { | |
| try { | |
| Stop-Process -Id $procId -ErrorAction Stop | |
| } catch { | |
| Write-ColorMessage -Level WARN -Message "無法終止進程 $procId,嘗試強制終止" | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| Start-Sleep -Seconds 2 | |
| Write-ColorMessage -Level INFO -Message "✓ rclone 進程已終止" | |
| # Windows 下卸載後不刪除掛載點目錄 | |
| # 因為 rclone 在掛載前就要求路徑不存在,卸載後也不該主動刪除 | |
| # 如果目錄存在,表示可能有其他用途或是遺留檔案 | |
| if (Test-Path $MountPoint) { | |
| Write-ColorMessage -Level INFO -Message "掛載點目錄仍存在: $MountPoint (未自動刪除)" | |
| } else { | |
| Write-ColorMessage -Level INFO -Message "掛載點已清除: $MountPoint" | |
| } | |
| return $true | |
| } | |
| <# | |
| .SYNOPSIS | |
| 卸載 remote | |
| #> | |
| function Dismount-RcloneRemote { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$CustomMountPoint = '' | |
| ) | |
| # 查找所有掛載點 | |
| $mountInfo = Find-MountPoints -HostAlias $HostAlias | |
| if ($mountInfo.Count -eq 0) { | |
| Write-ColorMessage -Level WARN -Message "$HostAlias 未掛載" | |
| # 檢查是否還有相關的 rclone 進程在運行 | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias | |
| if ($rclonePids.Count -gt 0) { | |
| Write-ColorMessage -Level INFO -Message "發現殘留的 rclone 進程: $($rclonePids -join ', ')" | |
| Write-ColorMessage -Level INFO -Message "正在終止進程..." | |
| foreach ($procId in $rclonePids) { | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| Start-Sleep -Seconds 2 | |
| Write-ColorMessage -Level INFO -Message "✓ rclone 進程已終止" | |
| } | |
| return $true | |
| } | |
| # 如果指定了掛載點,只卸載匹配的 | |
| if ($CustomMountPoint) { | |
| $found = $false | |
| foreach ($mount in $mountInfo) { | |
| if ($mount.MountPoint -eq $CustomMountPoint) { | |
| $found = $true | |
| Invoke-Unmount -MountPoint $mount.MountPoint -HostAlias $HostAlias | |
| break | |
| } | |
| } | |
| if (-not $found) { | |
| Write-ColorMessage -Level ERROR -Message "找不到掛載的路徑: $HostAlias $CustomMountPoint" | |
| Write-ColorMessage -Level INFO -Message "當前 $HostAlias 的掛載:" | |
| foreach ($mount in $mountInfo) { | |
| $pathDisplay = if ($mount.RemotePath) { $mount.RemotePath } else { "~" } | |
| Write-ColorMessage -Level INFO -Message " [$pathDisplay] → $($mount.MountPoint)" | |
| } | |
| return $false | |
| } | |
| } else { | |
| # 未指定掛載點,逐筆詢問確認 | |
| Write-ColorMessage -Level INFO -Message "找到 $HostAlias 的 $($mountInfo.Count) 個掛載:" | |
| foreach ($mount in $mountInfo) { | |
| $pathDisplay = if ($mount.RemotePath) { $mount.RemotePath } else { "~" } | |
| Write-Host "" | |
| Write-ColorMessage -Level INFO -Message "掛載: [$pathDisplay] on $($mount.MountPoint)" | |
| $answer = Read-Host "是否要卸載此掛載? [y/N]" | |
| if ($answer -match '^[Yy]$') { | |
| Invoke-Unmount -MountPoint $mount.MountPoint -HostAlias $HostAlias | |
| } else { | |
| Write-ColorMessage -Level INFO -Message "跳過 $($mount.MountPoint)" | |
| } | |
| } | |
| } | |
| return $true | |
| } | |
| # ==================== 核心函數: 狀態檢查 ==================== | |
| <# | |
| .SYNOPSIS | |
| 檢查掛載狀態 | |
| #> | |
| function Get-RcloneStatus { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$RemotePath = '' | |
| ) | |
| Write-ColorMessage -Level INFO -Message "檢查 $HostAlias 的掛載狀態" | |
| # 查找所有掛載點 | |
| $mountInfo = Find-MountPoints -HostAlias $HostAlias | |
| if ($mountInfo.Count -eq 0) { | |
| Write-ColorMessage -Level INFO -Message "✗ 未掛載" | |
| return | |
| } | |
| # 如果指定了 RemotePath,只顯示匹配的 | |
| if ($RemotePath) { | |
| $found = $false | |
| foreach ($mount in $mountInfo) { | |
| if ($mount.RemotePath -eq $RemotePath) { | |
| $found = $true | |
| Write-ColorMessage -Level INFO -Message "✓ 已掛載: $($mount.MountPoint)" | |
| # 檢查掛載點是否可訪問 | |
| try { | |
| $null = Get-ChildItem -Path $mount.MountPoint -ErrorAction Stop | |
| Write-ColorMessage -Level INFO -Message "✓ 掛載點可正常訪問" | |
| } catch { | |
| Write-ColorMessage -Level WARN -Message "⚠ 掛載點無法訪問,可能需要重新掛載" | |
| } | |
| } | |
| } | |
| if (-not $found) { | |
| Write-ColorMessage -Level INFO -Message "✗ 指定路徑 $RemotePath 未掛載" | |
| } | |
| } else { | |
| # 顯示所有掛載 | |
| Write-ColorMessage -Level INFO -Message "找到 $($mountInfo.Count) 個掛載:" | |
| foreach ($mount in $mountInfo) { | |
| $pathDisplay = if ($mount.RemotePath) { $mount.RemotePath } else { "~" } | |
| Write-ColorMessage -Level INFO -Message " [$pathDisplay] → $($mount.MountPoint)" | |
| # 檢查掛載點是否可訪問 | |
| try { | |
| $null = Get-ChildItem -Path $mount.MountPoint -ErrorAction Stop | |
| Write-ColorMessage -Level INFO -Message " ✓ 可訪問" | |
| } catch { | |
| Write-ColorMessage -Level WARN -Message " ✗ 無法訪問" | |
| } | |
| } | |
| } | |
| } | |
| # ==================== 輔助函數: 批量操作 ==================== | |
| <# | |
| .SYNOPSIS | |
| 批量掛載所有可用的 SSH hosts | |
| #> | |
| function Mount-AllRemotes { | |
| [CmdletBinding()] | |
| param() | |
| $successCount = 0 | |
| $failCount = 0 | |
| $failedHosts = @() | |
| Write-ColorMessage -Level INFO -Message "獲取所有可用的 SSH Host..." | |
| # 獲取所有可用的 hosts | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| Write-ColorMessage -Level WARN -Message "SSH config 不存在: $sshConfigPath" | |
| return | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $allHosts = $content | Where-Object { $_ -match '^\s*Host\s+(.+)$' } | ForEach-Object { | |
| $Matches[1] -split '\s+' | Where-Object { $_ -notmatch '\*' -and $_ -ne 'github.com' } | |
| } | Select-Object -Unique | Sort-Object | |
| if ($allHosts.Count -eq 0) { | |
| Write-ColorMessage -Level WARN -Message "沒有可掛載的 SSH Host" | |
| return | |
| } | |
| $totalHosts = $allHosts.Count | |
| Write-ColorMessage -Level INFO -Message "找到 $totalHosts 個 SSH Host,開始批量掛載..." | |
| Write-Host "" | |
| $current = 0 | |
| foreach ($sshHost in $allHosts) { | |
| $current++ | |
| $mountPoint = Join-Path $script:Config.MountBaseDir $sshHost | |
| # 檢查是否已掛載 | |
| if (Test-Mounted -MountPoint $mountPoint) { | |
| Write-ColorMessage -Level INFO -Message "[$current/$totalHosts] $sshHost 已經掛載在 $mountPoint,跳過" | |
| $successCount++ | |
| continue | |
| } | |
| Write-ColorMessage -Level INFO -Message "==== [$current/$totalHosts] 掛載 $sshHost ====" | |
| # 使用核心掛載函數 | |
| if (Invoke-MountSingle -HostAlias $sshHost -RemotePath '' -MountPoint $mountPoint) { | |
| $successCount++ | |
| Write-ColorMessage -Level INFO -Message "✓ $sshHost 掛載成功" | |
| } else { | |
| $failCount++ | |
| $failedHosts += $sshHost | |
| Write-ColorMessage -Level WARN -Message "✗ $sshHost 掛載失敗,繼續下一個" | |
| } | |
| Write-Host "" | |
| } | |
| # 顯示統計 | |
| Write-Host "" | |
| Write-ColorMessage -Level INFO -Message "==== 批量掛載完成 ====" | |
| Write-ColorMessage -Level INFO -Message "成功: $successCount, 失敗: $failCount, 總計: $totalHosts" | |
| if ($failCount -gt 0) { | |
| Write-ColorMessage -Level WARN -Message "失敗的 hosts: $($failedHosts -join ', ')" | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 批量卸載所有 rclone 掛載 | |
| #> | |
| function Dismount-AllRemotes { | |
| [CmdletBinding()] | |
| param() | |
| $successCount = 0 | |
| $failCount = 0 | |
| $failedMounts = @() | |
| Write-ColorMessage -Level INFO -Message "檢查所有 rclone 掛載..." | |
| # 獲取所有 rclone 進程 | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| Write-ColorMessage -Level INFO -Message "目前沒有 rclone 掛載" | |
| return | |
| } | |
| $allMounts = @() | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| # 使用 -match 搭配變數來避免解析問題 | |
| $pattern = 'mount\s+"?([^":]+):([^"]*)"?\s+"?([^"]+)"?' | |
| if ($cmdLine -match $pattern) { | |
| $allMounts += [PSCustomObject]@{ | |
| HostAlias = $Matches[1] | |
| RemotePath = $Matches[2] | |
| MountPoint = $Matches[3] | |
| ProcessId = $process.Id | |
| } | |
| } | |
| } catch { | |
| # 忽略錯誤 | |
| } | |
| } | |
| if ($allMounts.Count -eq 0) { | |
| Write-ColorMessage -Level INFO -Message "目前沒有 rclone 掛載" | |
| return | |
| } | |
| $totalMounts = $allMounts.Count | |
| Write-ColorMessage -Level INFO -Message "找到 $totalMounts 個 rclone 掛載,開始批量卸載..." | |
| Write-Host "" | |
| $current = 0 | |
| foreach ($mount in $allMounts) { | |
| $current++ | |
| Write-ColorMessage -Level INFO -Message "==== [$current/$totalMounts] 卸載 $($mount.MountPoint) ====" | |
| # 使用核心卸載函數 | |
| if (Invoke-Unmount -MountPoint $mount.MountPoint -HostAlias $mount.HostAlias) { | |
| $successCount++ | |
| Write-ColorMessage -Level INFO -Message "✓ $($mount.MountPoint) 卸載成功" | |
| } else { | |
| $failCount++ | |
| $failedMounts += $mount.MountPoint | |
| Write-ColorMessage -Level WARN -Message "✗ $($mount.MountPoint) 卸載失敗,繼續下一個" | |
| } | |
| Write-Host "" | |
| } | |
| # 顯示統計 | |
| Write-Host "" | |
| Write-ColorMessage -Level INFO -Message "==== 批量卸載完成 ====" | |
| Write-ColorMessage -Level INFO -Message "成功: $successCount, 失敗: $failCount, 總計: $totalMounts" | |
| if ($failCount -gt 0) { | |
| Write-ColorMessage -Level WARN -Message "失敗的掛載點: $($failedMounts -join ', ')" | |
| } | |
| } | |
| # ==================== 輔助函數: 其他功能 ==================== | |
| <# | |
| .SYNOPSIS | |
| 顯示所有掛載中的 rclone | |
| #> | |
| function Show-RcloneMounts { | |
| [CmdletBinding()] | |
| param() | |
| Write-ColorMessage -Level INFO -Message "檢查所有 rclone 掛載..." | |
| Write-Host "" | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| Write-ColorMessage -Level INFO -Message "目前沒有 rclone 掛載" | |
| return | |
| } | |
| Write-Host "=== 已掛載的 rclone 遠端 ===" -ForegroundColor Green | |
| Write-Host "" | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| $pattern = 'mount\s+"?([^":]+):([^"]*)"?\s+"?([^"]+)"?' | |
| if ($cmdLine -match $pattern) { | |
| $remote = $Matches[1] | |
| $remotePath = $Matches[2] | |
| $mountPoint = $Matches[3] | |
| Write-Host "Remote: " -ForegroundColor Yellow -NoNewline | |
| Write-Host $remote | |
| Write-Host "路徑: " -ForegroundColor Yellow -NoNewline | |
| Write-Host $(if ($remotePath) { $remotePath } else { "~" }) | |
| Write-Host "掛載點: " -ForegroundColor Yellow -NoNewline | |
| Write-Host $mountPoint | |
| # 測試掛載點訪問 | |
| try { | |
| $null = Get-ChildItem -Path $mountPoint -ErrorAction Stop | |
| Write-Host "狀態: " -ForegroundColor Yellow -NoNewline | |
| Write-Host "✓ 可訪問" -ForegroundColor Green | |
| } catch { | |
| Write-Host "狀態: " -ForegroundColor Yellow -NoNewline | |
| Write-Host "✗ 無法訪問" -ForegroundColor Red | |
| } | |
| Write-Host "" | |
| } | |
| } catch { | |
| # 忽略錯誤 | |
| } | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 獲取可用的 SSH Host 清單 | |
| #> | |
| function Get-AvailableRemotes { | |
| [CmdletBinding()] | |
| param() | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $sshHosts = $content | Where-Object { $_ -match '^\s*Host\s+(.+)$' } | ForEach-Object { | |
| $Matches[1] -split '\s+' | Where-Object { $_ -notmatch '\*' -and $_ -ne 'github.com' } | |
| } | Select-Object -Unique | Sort-Object | |
| foreach ($sshHost in $sshHosts) { | |
| # 檢查此 host 是否已掛載 | |
| $mountInfo = Find-MountPoints -HostAlias $sshHost | |
| if ($mountInfo.Count -gt 0) { | |
| $firstMount = $mountInfo[0].MountPoint | |
| if ($mountInfo.Count -gt 1) { | |
| $status = "已掛載 {0} 個: {1} ..." -f $mountInfo.Count, $firstMount | |
| Write-Host "$sshHost [$status]" -ForegroundColor Yellow | |
| } else { | |
| $status = "已掛載: {0}" -f $firstMount | |
| Write-Host "$sshHost [$status]" -ForegroundColor Yellow | |
| } | |
| } else { | |
| Write-Host $sshHost | |
| } | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 顯示使用說明 | |
| #> | |
| function Show-Help { | |
| [CmdletBinding()] | |
| param() | |
| Write-Host "rclone Mount 管理工具" -ForegroundColor Cyan | |
| Write-Host "" | |
| Write-Host "使用方式:" -ForegroundColor Yellow | |
| Write-Host ' .\rclonemm.ps1 <operation> <remote_name>[:<remote_path>] [custom_mount_point]' | |
| Write-Host ' .\rclonemm.ps1 <operation> --all|--a' | |
| Write-Host "" | |
| Write-Host "參數:" -ForegroundColor Yellow | |
| Write-Host " operation 操作類型" | |
| Write-Host " - mount/mnt: 掛載遠端儲存" | |
| Write-Host " - unmount/umount/umnt: 卸載掛載點" | |
| Write-Host " - status: 檢查掛載狀態" | |
| Write-Host " - show: 顯示所有掛載中的 rclone" | |
| Write-Host " - help: 顯示此說明" | |
| Write-Host " remote_name SSH config 中的 Host 名稱" | |
| Write-Host " remote_path 選填,遠端路徑(僅用於 mount 命令)" | |
| Write-Host " custom_mount_point 選填,自定義本地掛載點(僅用於 mount 命令)" | |
| Write-Host " --all, --a 批量操作所有 hosts(mount/unmount 支援)" | |
| Write-Host "" | |
| Write-Host "單一掛載範例:" -ForegroundColor Yellow | |
| Write-Host " .\rclonemm.ps1 mount mom # 掛載 mom 根目錄到預設位置" | |
| Write-Host " .\rclonemm.ps1 mount mom:/WEB/logs # 掛載 mom 指定目錄到預設位置" | |
| Write-Host " .\rclonemm.ps1 mount mom D:\custom\path # 掛載 mom 根目錄到自定義位置" | |
| Write-Host " .\rclonemm.ps1 mount mom:/WEB D:\custom\path # 掛載 mom 指定目錄到自定義位置" | |
| Write-Host " .\rclonemm.ps1 unmount mom # 卸載 mom(自動找到掛載點)" | |
| Write-Host " .\rclonemm.ps1 status mom # 檢查 mom 狀態(自動找到掛載點)" | |
| Write-Host "" | |
| Write-Host "批量操作範例:" -ForegroundColor Yellow | |
| Write-Host " .\rclonemm.ps1 mount --all # 掛載所有 SSH Host(失敗自動跳過)" | |
| Write-Host " .\rclonemm.ps1 mount --a # 同上(簡寫)" | |
| Write-Host " .\rclonemm.ps1 unmount --all # 卸載所有 rclone 掛載" | |
| Write-Host " .\rclonemm.ps1 umount --a # 同上(簡寫)" | |
| Write-Host "" | |
| Write-Host "其他範例:" -ForegroundColor Yellow | |
| Write-Host " .\rclonemm.ps1 show # 顯示所有掛載" | |
| Write-Host " .\rclonemm.ps1 help # 顯示此說明" | |
| Write-Host "" | |
| Write-Host "可用的 SSH Hosts:" -ForegroundColor Yellow | |
| Get-AvailableRemotes | |
| } | |
| <# | |
| .SYNOPSIS | |
| 檢查 SSH Host 是否存在於設定檔中 | |
| #> | |
| function Test-RemoteExists { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| Write-ColorMessage -Level ERROR -Message "SSH config 不存在: $sshConfigPath" | |
| return $false | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $found = $content | Where-Object { $_ -match "^\s*Host\s+.*\b$HostAlias\b" } | |
| if (-not $found) { | |
| Write-ColorMessage -Level ERROR -Message "SSH Host '$HostAlias' 不存在於 SSH config 中" | |
| Write-Host "可用的 Host: " -NoNewline | |
| Get-AvailableRemotes | |
| return $false | |
| } | |
| return $true | |
| } | |
| # ==================== 主程式 ==================== | |
| function Main { | |
| # 檢查依賴 | |
| Test-Dependencies | |
| # 清理過期日誌 | |
| Clear-OldLogs | |
| # 檢查參數 | |
| if (-not $Operation) { | |
| Write-ColorMessage -Level ERROR -Message "缺少必要參數" | |
| Show-Help | |
| exit 1 | |
| } | |
| # 處理 help 操作 | |
| if ($Operation -in @('help', '-h', '--help')) { | |
| Show-Help | |
| exit 0 | |
| } | |
| # 處理 show 操作(不需要 remote 名稱) | |
| if ($Operation -eq 'show') { | |
| Show-RcloneMounts | |
| exit 0 | |
| } | |
| # 處理批量操作(--all 或 -a) | |
| if ($RemoteSpec -in @('--all', '--a')) { | |
| # 確保日誌目錄存在 | |
| $logDir = Split-Path $script:LogFile -Parent | |
| if (-not (Test-Path $logDir)) { | |
| New-Item -Path $logDir -ItemType Directory -Force | Out-Null | |
| } | |
| Write-ColorMessage -Level INFO -Message "批量操作: $Operation 所有 remotes,日誌: $($script:LogFile)" | |
| switch ($Operation) { | |
| { $_ -in @('mount', 'mnt') } { | |
| Mount-AllRemotes | |
| exit 0 | |
| } | |
| { $_ -in @('unmount', 'umount', 'umnt') } { | |
| Dismount-AllRemotes | |
| exit 0 | |
| } | |
| default { | |
| Write-ColorMessage -Level ERROR -Message "操作 '$Operation' 不支援 --all/--a 參數" | |
| Write-ColorMessage -Level INFO -Message "支援 --all/--a 的操作: mount, unmount" | |
| exit 1 | |
| } | |
| } | |
| } | |
| # 檢查是否提供 remote 規格 | |
| if (-not $RemoteSpec) { | |
| Write-ColorMessage -Level ERROR -Message "缺少 SSH Host 名稱參數或 --all/--a 選項" | |
| Show-Help | |
| exit 1 | |
| } | |
| # 解析 host alias 和路徑 | |
| # 格式: host_alias 或 host_alias:path | |
| $hostAlias = '' | |
| $remotePath = '' | |
| $pattern = '^(.+?):(.*)$' | |
| if ($RemoteSpec -match $pattern) { | |
| $hostAlias = $Matches[1] | |
| $remotePath = $Matches[2] | |
| } else { | |
| $hostAlias = $RemoteSpec | |
| $remotePath = '' | |
| } | |
| # 檢查 SSH Host 是否存在 | |
| if (-not (Test-RemoteExists -HostAlias $hostAlias)) { | |
| exit 1 | |
| } | |
| # 確保日誌目錄存在 | |
| $logDir = Split-Path $script:LogFile -Parent | |
| if (-not (Test-Path $logDir)) { | |
| New-Item -Path $logDir -ItemType Directory -Force | Out-Null | |
| } | |
| # 執行對應操作 | |
| Write-ColorMessage -Level INFO -Message "操作: $Operation,日誌: $($script:LogFile)" | |
| switch ($Operation) { | |
| { $_ -in @('mount', 'mnt') } { | |
| Mount-RcloneRemote -HostAlias $hostAlias -RemotePath $remotePath -CustomMountPoint $CustomMountPoint | |
| } | |
| { $_ -in @('unmount', 'umount', 'umnt') } { | |
| Dismount-RcloneRemote -HostAlias $hostAlias -CustomMountPoint $CustomMountPoint | |
| } | |
| 'status' { | |
| Get-RcloneStatus -HostAlias $hostAlias -RemotePath $remotePath | |
| } | |
| default { | |
| Write-ColorMessage -Level ERROR -Message "未知操作: $Operation" | |
| Show-Help | |
| exit 1 | |
| } | |
| } | |
| } | |
| # 執行主程式 | |
| Main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # rclone mount 管理腳本 | |
| # 功能: mount, unmount, status, help | |
| # 使用方式: rclonemm [operation] <remote_name>[:<remote_path>] [custom_mount_point] | |
| # 範例:rclonemm mount mom:/WEB/logs /custom/path | |
| # 作者: GitHub Copilot | |
| # 日期: 2025-11-04 | |
| # 更新: 2025-11-12 - 新增自定義掛載點支援 | |
| # 更新: 2025-12-19 - 改用 SSH config,不再依賴 rclone.conf | |
| set -euo pipefail | |
| # 常數 | |
| # 設定檔案路徑 | |
| # 根據環境變數設定掛載基礎目錄 | |
| if [[ -n "${PREFIX:-}" ]]; then | |
| echo "使用自定義 PREFIX: $PREFIX" | |
| else | |
| PREFIX="" | |
| fi | |
| SSH_CONFIG="$HOME/.ssh/config" | |
| MOUNT_BASE_DIR="$PREFIX/mnt" | |
| LOG_DIR="$PREFIX/var/log" | |
| LOG_FILE="${LOG_DIR}/rclone_mount_$(date '+%Y%m%d').log" | |
| # 變數 | |
| # 日誌保留天數 | |
| LOG_RETENTION_DAYS=7 | |
| # 掛載超時設定(秒) | |
| MOUNT_TIMEOUT=10 | |
| ACCESS_TEST_TIMEOUT=2 | |
| # 獲取當前用戶的 UID 和 GID | |
| USER="${USER:-$(whoami)}" | |
| USER_UID=$(id -u "$USER") | |
| USER_GID=$(id -g "$USER") | |
| # 基本 RCLONE 參數 | |
| RCLONE_MOUNT_OPTIONS="--allow-other \ | |
| --vfs-cache-mode full \ | |
| --default-permissions \ | |
| --uid $USER_UID \ | |
| --gid $USER_GID \ | |
| --multi-thread-streams 4 \ | |
| --multi-thread-cutoff 100M \ | |
| --low-level-retries 2 \ | |
| --log-level ERROR \ | |
| --log-file $LOG_FILE" | |
| # 基本 SSH 命令 | |
| ssh_cmd="ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%r@%h:%p -o ControlPersist=10 -o ConnectTimeout=2" | |
| # 依賴檢查函數 | |
| check_dependencies() { | |
| local missing_deps=() | |
| # 檢查 rclone | |
| if ! command -v rclone >/dev/null 2>&1; then | |
| missing_deps+=("rclone") | |
| fi | |
| # 檢查 fusermount 或 fusermount3 | |
| if ! command -v fusermount >/dev/null 2>&1 && ! command -v fusermount3 >/dev/null 2>&1; then | |
| missing_deps+=("fusermount (fuse)") | |
| fi | |
| # 檢查 sudo | |
| if ! command -v sudo >/dev/null 2>&1; then | |
| missing_deps+=("sudo") | |
| fi | |
| # 如果有缺失的依賴,顯示錯誤並退出 | |
| if [[ ${#missing_deps[@]} -gt 0 ]]; then | |
| echo -e "${RED}[ERROR]${NC} 缺少必要的依賴套件:" | |
| for dep in "${missing_deps[@]}"; do | |
| echo -e " ${RED}✗${NC} $dep" | |
| done | |
| echo | |
| echo -e "${YELLOW}請安裝缺少的套件:${NC}" | |
| echo " Debian/Ubuntu: sudo apt-get install rclone fuse3 sudo" | |
| echo " Alpine: apk add rclone fuse3 sudo" | |
| echo " Termux: pkg install rclone libfuse3 tsu" | |
| exit 1 | |
| fi | |
| } | |
| # 檢查 ps 命令是否可用 | |
| has_ps_command() { | |
| command -v ps >/dev/null 2>&1 | |
| } | |
| # 安全地執行 ps 命令 | |
| safe_ps() { | |
| if has_ps_command; then | |
| safe_ps 2>/dev/null || ps -ef 2>/dev/null | |
| else | |
| # 沒有 ps 命令,返回空 | |
| return 1 | |
| fi | |
| } | |
| # === SSH Config 解析函數 === | |
| # 解析 SSH config 獲取指定 Host 的指定屬性 | |
| parse_ssh_config() { | |
| local host_alias="$1" | |
| local key="$2" | |
| awk -v host="$host_alias" -v key="$key" ' | |
| tolower($1) == "host" { | |
| for (i = 2; i <= NF; i++) { | |
| if ($i == host) { | |
| found = 1 | |
| next | |
| } | |
| } | |
| found = 0 | |
| } | |
| found && tolower($1) == tolower(key) { | |
| print $2 | |
| exit | |
| } | |
| ' "$SSH_CONFIG" | |
| } | |
| # 獲取 SSH Host 的完整設定 | |
| get_ssh_host_config() { | |
| local host_alias="$1" | |
| local hostname user port identity_file proxy_jump | |
| hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| user=$(parse_ssh_config "$host_alias" "User") | |
| port=$(parse_ssh_config "$host_alias" "Port") | |
| identity_file=$(parse_ssh_config "$host_alias" "IdentityFile") | |
| proxy_jump=$(parse_ssh_config "$host_alias" "ProxyJump") | |
| # 如果沒有明確的 HostName,使用 Host alias | |
| hostname="${hostname:-$host_alias}" | |
| # 展開 ~ 為實際路徑 | |
| if [[ -n "$identity_file" ]]; then | |
| identity_file="${identity_file/#\~/$HOME}" | |
| fi | |
| # 輸出格式: hostname|user|port|identity_file|proxy_jump | |
| echo "$hostname|$user|$port|$identity_file|$proxy_jump" | |
| } | |
| # 組建 rclone SFTP backend 字串(使用 key_file) | |
| # 參數: $1 = host_alias 或 config_data | |
| build_sftp_backend_with_key() { | |
| local input="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| # 判斷輸入是 host_alias 還是 config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # 已經是 config_data | |
| config_data="$input" | |
| else | |
| # 是 host_alias,需要取得 config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # 基本 backend 字串 | |
| local backend=":sftp,host=$hostname" | |
| # 添加可選參數 | |
| [[ -n "$user" ]] && backend="$backend,user=$user" | |
| [[ -n "$port" ]] && backend="$backend,port=$port" | |
| [[ -n "$key_file" ]] && backend="$backend,key_file=$key_file" | |
| echo "$backend" | |
| } | |
| # 組建 rclone SFTP backend 字串(不使用 key_file,配合 --sftp-ssh) | |
| # 參數: $1 = host_alias 或 config_data | |
| build_sftp_backend_without_key() { | |
| local input="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| # 判斷輸入是 host_alias 還是 config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # 已經是 config_data | |
| config_data="$input" | |
| else | |
| # 是 host_alias,需要取得 config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # 基本 backend 字串 | |
| local backend=":sftp,host=$hostname" | |
| # 添加可選參數 | |
| [[ -n "$user" ]] && backend="$backend,user=$user" | |
| [[ -n "$port" ]] && backend="$backend,port=$port" | |
| echo "$backend" | |
| } | |
| # 檢查是否有 key file | |
| # 參數: $1 = host_alias 或 config_data | |
| has_key_file() { | |
| local input="$1" | |
| local config_data key_file | |
| # 判斷輸入是 host_alias 還是 config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # 已經是 config_data | |
| config_data="$input" | |
| else | |
| # 是 host_alias,需要取得 config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r _ _ _ key_file _ <<< "$config_data" | |
| [[ -n "$key_file" ]] && [[ -f "$key_file" ]] | |
| } | |
| # 組建 --sftp-ssh 選項 | |
| # 參數: $1 = host_alias 或 config_data | |
| build_sftp_ssh_option() { | |
| local input="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| # 判斷輸入是 host_alias 還是 config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # 已經是 config_data | |
| config_data="$input" | |
| else | |
| # 是 host_alias,需要取得 config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # 添加 Port | |
| if [[ -n "$port" ]]; then | |
| ssh_cmd="$ssh_cmd -p $port" | |
| fi | |
| # 添加 IdentityFile | |
| if [[ -n "$key_file" ]]; then | |
| ssh_cmd="$ssh_cmd -i $key_file" | |
| fi | |
| # 添加 ProxyJump | |
| if [[ -n "$proxy_jump" ]]; then | |
| ssh_cmd="$ssh_cmd -J $proxy_jump" | |
| fi | |
| # 組建完整的 SSH 連線字串 user@hostname | |
| local ssh_target="" | |
| if [[ -n "$user" ]]; then | |
| ssh_target="${user}@${hostname}" | |
| else | |
| ssh_target="${hostname}" | |
| fi | |
| echo "--sftp-ssh \"$ssh_cmd $ssh_target\"" | |
| } | |
| # === 掛載點查找函數 === | |
| # 通過 mount 命令查找指定 host_alias 的所有掛載點 | |
| # 返回格式:每行包含 "remote_path|mount_point" | |
| find_mount_points_by_mount() { | |
| local host_alias="$1" | |
| # 獲取 hostname | |
| # local hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| # hostname="${hostname:-$host_alias}" | |
| # 從 mount 輸出中查找所有匹配的掛載 | |
| # 支援格式:hostname: 或 hostname{xxx}: | |
| mount | grep "type fuse.rclone" 2>/dev/null | while read -r line; do | |
| [[ -z "$line" ]] && continue | |
| local remote_part | |
| remote_part=$(echo "$line" | awk '{print $1}') | |
| local mount_point | |
| mount_point=$(echo "$line" | awk '{print $3}') | |
| # 檢查 remote_part 是否以 hostname 開頭 | |
| # 支援 hostname: 或 hostname{xxx}: 格式 | |
| if [[ "$remote_part" =~ ^${host_alias}(\{[^}]+\})?:(.*)$ ]]; then | |
| # 提取 remote_path(冒號後的部分) | |
| local remote_path="${BASH_REMATCH[2]}" | |
| # 輸出格式:remote_path|mount_point | |
| echo "${remote_path}|${mount_point}" | |
| fi | |
| done || true | |
| } | |
| # 查找匹配 host_alias 和掛載點的 rclone 進程 PID | |
| # 參數: $1 = host_alias, $2 = mount_point (可選) | |
| find_rclone_pids() { | |
| local host_alias="$1" | |
| local mount_point="${2:-}" | |
| if [[ -n "$mount_point" ]]; then | |
| # 同時匹配 host_alias 和掛載點 | |
| safe_ps | grep "rclone.*$host_alias" | grep "$mount_point" | grep -v grep | grep -v "rclonemm" | awk '{print $1}' | grep -E '^[0-9]+$' | |
| else | |
| # 只匹配 host_alias | |
| safe_ps | grep "rclone.*$host_alias" | grep -v grep | grep -v "rclonemm" | awk '{print $1}' | grep -E '^[0-9]+$' | |
| fi | |
| } | |
| # 顏色定義 | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| # 清理過期日誌 | |
| cleanup_old_logs() { | |
| # 確保日誌目錄存在 | |
| if [[ ! -d "$LOG_DIR" ]]; then | |
| return 0 | |
| fi | |
| # 刪除超過保留天數的日誌檔案 | |
| # 格式: rclone_mount_YYYYMMDD.log | |
| sudo find "$LOG_DIR" -name "rclone_mount_*.log" -type f -mtime +${LOG_RETENTION_DAYS} -delete 2>/dev/null || true | |
| } | |
| # 顯示使用說明 | |
| show_help() { | |
| echo -e "${BLUE}rclone Mount 管理工具${NC}" | |
| echo | |
| echo -e "${YELLOW}使用方式:${NC}" | |
| echo " $(basename "$0") <operation> <remote_name>[:<remote_path>] [custom_mount_point]" | |
| echo " $(basename "$0") <operation> --all|-a" | |
| echo | |
| echo -e "${YELLOW}參數:${NC}" | |
| echo " operation 操作類型" | |
| echo " - mount/mnt: 掛載遠端儲存" | |
| echo " - unmount/umount/umnt: 卸載掛載點" | |
| echo " - status: 檢查掛載狀態" | |
| echo " - show: 顯示所有掛載中的rclone" | |
| echo " - help: 顯示此說明" | |
| echo " remote_name SSH config 中的 Host 名稱" | |
| echo " remote_path 選填,遠端路徑(僅用於mount命令)" | |
| echo " custom_mount_point 選填,自定義本地掛載點(僅用於mount命令)" | |
| echo " --all, -a 批量操作所有hosts(mount/unmount支援)" | |
| echo | |
| echo -e "${YELLOW}單一掛載範例:${NC}" | |
| echo " $(basename "$0") mount mom # 掛載mom根目錄到預設位置" | |
| echo " $(basename "$0") mount mom:/WEB/logs # 掛載mom指定目錄到預設位置" | |
| echo " $(basename "$0") mount mom /custom/path # 掛載mom根目錄到自定義位置" | |
| echo " $(basename "$0") mount mom:/WEB /custom/path # 掛載mom指定目錄到自定義位置" | |
| echo " $(basename "$0") unmount mom # 卸載mom(自動找到掛載點)" | |
| echo " $(basename "$0") status mom # 檢查mom狀態(自動找到掛載點)" | |
| echo | |
| echo -e "${YELLOW}批量操作範例:${NC}" | |
| echo " $(basename "$0") mount --all # 掛載所有SSH Host(失敗自動跳過)" | |
| echo " $(basename "$0") mount -a # 同上(簡寫)" | |
| echo " $(basename "$0") unmount --all # 卸載所有rclone掛載" | |
| echo " $(basename "$0") umount -a # 同上(簡寫)" | |
| echo | |
| echo -e "${YELLOW}其他範例:${NC}" | |
| echo " $(basename "$0") show # 顯示所有掛載" | |
| echo " $(basename "$0") help # 顯示此說明" | |
| echo | |
| echo -e "${YELLOW}可用的 SSH Hosts:${NC}" | |
| get_available_remotes | |
| } | |
| # 記錄日誌 | |
| log_message() { | |
| local level="$1" | |
| local message="$2" | |
| local timestamp | |
| timestamp=$(date '+%Y-%m-%d %H:%M:%S') | |
| echo "[$timestamp] [$level] $message" | sudo tee -a "$LOG_FILE" >/dev/null 2>&1 || true | |
| } | |
| # 輸出訊息 | |
| print_message() { | |
| local level="$1" | |
| local message="$2" | |
| local color="" | |
| case "$level" in | |
| "INFO") color="$GREEN" ;; | |
| "WARN") color="$YELLOW" ;; | |
| "ERROR") color="$RED" ;; | |
| "DEBUG") color="$BLUE" ;; | |
| esac | |
| echo -e "${color}[$level]${NC} $message" | |
| # 避免log_message失敗導致腳本退出 | |
| log_message "$level" "$message" || true | |
| } | |
| # 獲取可用的 SSH Host 清單 | |
| get_available_remotes() { | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| return 1 | |
| fi | |
| # 獲取所有 Host 名稱(排除萬用字元和特殊用途) | |
| local hosts | |
| hosts=$(grep "^Host " "$SSH_CONFIG" | awk '{print $2}' | grep -v '\*' | grep -v 'github.com' | sort) | |
| # 逐行處理每個 host | |
| while IFS= read -r host; do | |
| [[ -z "$host" ]] && continue | |
| # 檢查此 host 是否已掛載 | |
| local mount_info | |
| mount_info=$(find_mount_points_by_mount "$host") | |
| if [[ -n "$mount_info" ]]; then | |
| # 計算掛載數量 | |
| local mount_count | |
| mount_count=$(echo "$mount_info" | wc -l) | |
| # 取得第一個掛載點 | |
| local first_mount | |
| first_mount=$(echo "$mount_info" | head -1 | cut -d'|' -f2) | |
| if [[ $mount_count -gt 1 ]]; then | |
| echo -e "$host ${YELLOW}[已掛載 ${mount_count} 個: $first_mount ...]${NC}" | |
| else | |
| echo -e "$host ${YELLOW}[已掛載: $first_mount]${NC}" | |
| fi | |
| else | |
| echo "$host" | |
| fi | |
| done <<< "$hosts" | |
| } | |
| # 檢查 SSH Host 是否存在於設定檔中 | |
| check_remote_exists() { | |
| local host_alias="$1" | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| print_message "ERROR" "SSH config 不存在: $SSH_CONFIG" | |
| return 1 | |
| fi | |
| if ! grep -q "^Host .*\\b$host_alias\\b" "$SSH_CONFIG"; then | |
| print_message "ERROR" "SSH Host '$host_alias' 不存在於 SSH config 中" | |
| print_message "INFO" "可用的 Host: $(get_available_remotes | tr '\n' ' ')" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # 檢查並建立掛載點 | |
| ensure_mount_point() { | |
| local mount_point="$1" | |
| if [[ ! -d "$mount_point" ]]; then | |
| print_message "INFO" "建立掛載點: $mount_point" | |
| sudo mkdir -p "$mount_point" | |
| sudo chown "$USER:$USER" "$mount_point" | |
| fi | |
| # 檢查目錄權限 | |
| if [[ ! -w "$mount_point" ]]; then | |
| print_message "WARN" "掛載點沒有寫入權限,嘗試修正權限" | |
| sudo chown "$USER:$USER" "$mount_point" | |
| fi | |
| } | |
| # 檢查是否已掛載 | |
| is_mounted() { | |
| local mount_point="$1" | |
| mount | grep -q " $mount_point " || return 1 | |
| return 0 | |
| } | |
| # 核心掛載函數(單一 host 的掛載邏輯,包含 timeout 等待) | |
| # 參數: $1 = host_alias, $2 = remote_path (可選), $3 = mount_point | |
| # 返回: 0 成功, 1 失敗 | |
| do_mount_single() { | |
| local host_alias="$1" | |
| local remote_path="${2:-}" | |
| local mount_point="$3" | |
| local sftp_ssh_option="" | |
| local remote_source | |
| local config_data | |
| local hostname user port key_file proxy_jump | |
| # 取得 SSH Host 設定 | |
| config_data=$(get_ssh_host_config "$host_alias") | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # 統一使用 --sftp-ssh 選項進行掛載 | |
| sftp_ssh_option=$(build_sftp_ssh_option "$config_data") | |
| # 在掛載前先建立 rclone config(使用 pass 參數避免互動提示) | |
| if ! sudo rclone config create "$host_alias" sftp pass "" >/dev/null 2>&1; then | |
| print_message "ERROR" "建立 rclone config 失敗" | |
| return 1 | |
| fi | |
| # 組合遠端來源路徑 | |
| if [[ -n "$remote_path" ]]; then | |
| remote_source="${host_alias}:$remote_path" | |
| else | |
| remote_source="${host_alias}:" | |
| fi | |
| # 檢查目標掛載點是否被占用 | |
| if is_mounted "$mount_point"; then | |
| print_message "ERROR" "掛載點 $mount_point 已被其他檔案系統占用" | |
| local existing_mount | |
| existing_mount=$(mount | grep " $mount_point ") | |
| print_message "INFO" "現有掛載: $existing_mount" | |
| # 清除 rclone config | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 1 | |
| fi | |
| # 確保掛載點存在 | |
| ensure_mount_point "$mount_point" | |
| # 建構rclone指令 | |
| local cmd="sudo rclone mount $remote_source $mount_point" | |
| cmd="$cmd $RCLONE_MOUNT_OPTIONS" | |
| cmd="$cmd $sftp_ssh_option" | |
| cmd="$cmd --daemon" | |
| print_message "INFO" "掛載指令:$cmd" | |
| print_message "INFO" "提示: 如果 SSH 需要密碼,請在提示時輸入" | |
| # 執行掛載命令(容錯處理) | |
| # 使用變數捕獲退出碼,避免 set -e 導致腳本退出 | |
| # 注意:不恢復 set -e,讓調用者控制錯誤處理行為 | |
| local saved_errexit | |
| if [[ $- =~ e ]]; then | |
| saved_errexit=1 | |
| else | |
| saved_errexit=0 | |
| fi | |
| set +e | |
| eval "$cmd" 2>&1 | |
| local mount_exit_code=$? | |
| if [[ $saved_errexit -eq 1 ]]; then | |
| set -e | |
| fi | |
| if [[ $mount_exit_code -ne 0 ]]; then | |
| print_message "ERROR" "rclone mount 命令執行失敗 (退出碼: $mount_exit_code),詳細錯誤請查看日誌: $LOG_FILE" | |
| print_message "WARN" "執行清理操作..." | |
| # 清除 rclone config | |
| print_message "INFO" "清除 rclone config..." | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| # 刪除掛載點(如果是預設位置) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]] && [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "刪除掛載點: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" 2>/dev/null || true | |
| fi | |
| return 1 | |
| fi | |
| # 等待掛載完成 | |
| local count=0 | |
| while [[ $count -lt $MOUNT_TIMEOUT ]]; do | |
| print_message "INFO" "等待掛載中... ($((count + 1))/$MOUNT_TIMEOUT)" | |
| sleep 1 || true | |
| if is_mounted "$mount_point"; then | |
| print_message "INFO" "掛載成功: $host_alias:$remote_path -> $mount_point" | |
| # 測試掛載點是否可訪問 | |
| print_message "INFO" "測試掛載點訪問..." | |
| if timeout $ACCESS_TEST_TIMEOUT ls "$mount_point" >/dev/null 2>&1; then | |
| print_message "INFO" "✓ 掛載點測試通過,日誌: $LOG_FILE" | |
| # 掛載成功後清除 rclone config | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 0 | |
| else | |
| print_message "ERROR" "✗ 掛載點無法訪問,掛載失敗" | |
| print_message "WARN" "執行清理操作..." | |
| # 執行清理(不調用 unmount_remote 避免循環依賴) | |
| sudo umount "$mount_point" 2>/dev/null || sudo umount -f -l "$mount_point" 2>/dev/null || true | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| echo "$rclone_pids" | xargs sudo kill -TERM 2>/dev/null || true | |
| sleep 1 || true | |
| fi | |
| # 清除 rclone config | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 1 | |
| fi | |
| fi | |
| count=$((count + 1)) | |
| done | |
| print_message "ERROR" "掛載超時,請檢查日誌: $LOG_FILE" | |
| print_message "WARN" "執行清理操作..." | |
| # 執行清理 | |
| sudo umount "$mount_point" 2>/dev/null || sudo umount -f -l "$mount_point" 2>/dev/null || true | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| echo "$rclone_pids" | xargs sudo kill -TERM 2>/dev/null || true | |
| sleep 1 || true | |
| fi | |
| # 清除 rclone config | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 1 | |
| } | |
| # 掛載remote(用戶接口函數) | |
| mount_remote() { | |
| local host_alias="$1" | |
| local remote_path="${2:-}" | |
| local custom_mount_point="${3:-}" | |
| local mount_point | |
| # 決定掛載點:自定義 > 預設 | |
| if [[ -n "$custom_mount_point" ]]; then | |
| mount_point="$custom_mount_point" | |
| else | |
| mount_point="$MOUNT_BASE_DIR/$host_alias" | |
| fi | |
| print_message "INFO" "使用 --sftp-ssh 選項進行掛載" | |
| print_message "INFO" "掛載 $host_alias:$remote_path 到 $mount_point" | |
| if [[ -n "$custom_mount_point" ]]; then | |
| print_message "INFO" "使用自定義掛載點: $custom_mount_point" | |
| fi | |
| # 調用核心掛載函數 | |
| do_mount_single "$host_alias" "$remote_path" "$mount_point" | |
| return $? | |
| } | |
| # 卸載remote | |
| unmount_remote() { | |
| local host_alias="$1" | |
| local custom_mount_point="${2:-}" | |
| # local hostname | |
| # # 獲取 hostname 用於進程識別 | |
| # hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| # hostname="${hostname:-$host_alias}" | |
| # 查找所有掛載點 | |
| local mount_info | |
| mount_info=$(find_mount_points_by_mount "$host_alias") | |
| if [[ -z "$mount_info" ]]; then | |
| print_message "WARN" "$host_alias 未掛載" | |
| # 檢查是否還有相關的rclone進程在運行 | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias") | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "發現殘留的rclone進程: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "正在終止進程..." | |
| echo "$rclone_pids" | xargs sudo kill | |
| sleep 2 | |
| print_message "INFO" "✓ rclone進程已終止" | |
| fi | |
| return 0 | |
| fi | |
| # 如果指定了 remote_path,檢查是否存在 | |
| if [[ -n "$custom_mount_point" ]]; then | |
| local found=false | |
| local remote_path | |
| local target_mount_point="" | |
| while IFS='|' read -r existing_path existing_point; do | |
| if [[ "$existing_point" == "$custom_mount_point" ]]; then | |
| found=true | |
| target_mount_point="$existing_point" | |
| remote_path="$existing_path" | |
| break | |
| fi | |
| done <<< "$mount_info" | |
| if [[ "$found" == false ]]; then | |
| print_message "ERROR" "找不到掛載的路徑: $host_alias $custom_mount_point" | |
| print_message "INFO" "當前 $host_alias 的掛載:" | |
| while IFS='|' read -r existing_path existing_point; do | |
| local path_display="${existing_path:-~}" | |
| print_message "INFO" " [$path_display] → $existing_point" | |
| done <<< "$mount_info" | |
| return 1 | |
| fi | |
| # 執行單一卸載 | |
| print_message "INFO" "卸載 $host_alias:$remote_path on $custom_mount_point" | |
| perform_unmount "$target_mount_point" "$host_alias" | |
| return $? | |
| else | |
| # 未指定 remote_path,逐筆詢問確認 | |
| print_message "INFO" "找到 $host_alias 的 $(echo "$mount_info" | wc -l) 個掛載:" | |
| # 使用 process substitution 避免 subshell,讓 read 可以從 /dev/tty 讀取 | |
| while IFS='|' read -r existing_path existing_point; do | |
| local path_display="${existing_path:-~}" | |
| print_message "INFO" "" | |
| print_message "INFO" "掛載: [$path_display] on $existing_point" | |
| echo -n "是否要卸載此掛載? [y/N]: " | |
| # 從 /dev/tty 讀取以確保可以獲取用戶輸入 | |
| read -r answer </dev/tty || { | |
| echo | |
| print_message "WARN" "輸入被中斷,取消操作" | |
| return 1 | |
| } | |
| if [[ "$answer" =~ ^[Yy]$ ]]; then | |
| perform_unmount "$existing_point" "$host_alias" | |
| else | |
| print_message "INFO" "跳過 $existing_point" | |
| fi | |
| done <<< "$mount_info" | |
| return 0 | |
| fi | |
| } | |
| # 執行實際的卸載操作 | |
| perform_unmount() { | |
| local mount_point="$1" | |
| local host_alias="$2" | |
| # local hostname="$3" | |
| print_message "INFO" "卸載 $mount_point" | |
| # 嘗試正常卸載 | |
| if sudo umount "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "卸載成功: $mount_point" | |
| # 確保相關進程結束 | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "發現相關的rclone進程: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "正在終止進程..." | |
| echo "$rclone_pids" | xargs sudo kill 2>/dev/null || true | |
| sleep 1 || true | |
| print_message "INFO" "✓ rclone進程已終止" | |
| fi | |
| # 刪除預設掛載點,自定義掛載點嘗試刪除(如果為空) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]]; then | |
| if [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "刪除掛載點: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" | |
| fi | |
| else | |
| # 自定義掛載點:嘗試刪除空目錄,如果不為空則保留 | |
| if [[ -d "$mount_point" ]]; then | |
| if sudo rmdir "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "刪除空的自定義掛載點: $mount_point" | |
| else | |
| print_message "INFO" "保留非空的自定義掛載點: $mount_point" | |
| fi | |
| fi | |
| fi | |
| return 0 | |
| fi | |
| # 強制卸載 | |
| print_message "WARN" "正常卸載失敗,嘗試強制卸載" | |
| if sudo umount -f -l "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "強制卸載成功: $mount_point" | |
| # 確保相關進程結束 | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "發現相關的rclone進程: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "正在終止進程..." | |
| echo "$rclone_pids" | xargs sudo kill 2>/dev/null || true | |
| sleep 1 || true | |
| print_message "INFO" "✓ rclone進程已終止" | |
| fi | |
| # 刪除預設掛載點,自定義掛載點嘗試刪除(如果為空) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]]; then | |
| if [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "刪除掛載點: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" | |
| fi | |
| else | |
| # 自定義掛載點:嘗試刪除空目錄,如果不為空則保留 | |
| if [[ -d "$mount_point" ]]; then | |
| if sudo rmdir "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "刪除空的自定義掛載點: $mount_point" | |
| else | |
| print_message "INFO" "保留非空的自定義掛載點: $mount_point" | |
| fi | |
| fi | |
| fi | |
| return 0 | |
| fi | |
| # 最後手段:先kill rclone進程再嘗試卸載 | |
| print_message "WARN" "強制卸載失敗,嘗試終止rclone進程後再卸載" | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "發現rclone進程: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "正在發送TERM信號..." | |
| echo "$rclone_pids" | xargs sudo kill -TERM 2>/dev/null || true | |
| sleep 3 || true | |
| # 檢查進程是否還在運行,如果是則強制終止 | |
| local remaining_pids | |
| remaining_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$remaining_pids" ]]; then | |
| print_message "WARN" "進程仍在運行: $(echo "$remaining_pids" | tr '\n' ' ')" | |
| print_message "INFO" "正在發送KILL信號強制終止..." | |
| echo "$remaining_pids" | xargs sudo kill -KILL 2>/dev/null || true | |
| sleep 2 || true | |
| print_message "INFO" "✓ rclone進程已強制終止" | |
| else | |
| print_message "INFO" "✓ rclone進程已終止" | |
| fi | |
| # 再次嘗試卸載 | |
| sudo umount "$mount_point" 2>/dev/null || true | |
| print_message "INFO" "進程終止後卸載成功: $mount_point" | |
| # 刪除預設掛載點,自定義掛載點嘗試刪除(如果為空) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]]; then | |
| if [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "刪除掛載點: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" | |
| fi | |
| else | |
| # 自定義掛載點:嘗試刪除空目錄,如果不為空則保留 | |
| if [[ -d "$mount_point" ]]; then | |
| if sudo rmdir "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "刪除空的自定義掛載點: $mount_point" | |
| else | |
| print_message "INFO" "保留非空的自定義掛載點: $mount_point" | |
| fi | |
| fi | |
| fi | |
| return 0 | |
| fi | |
| print_message "ERROR" "卸載失敗: $mount_point" | |
| return 1 | |
| } | |
| # 檢查掛載狀態 | |
| check_status() { | |
| local host_alias="$1" | |
| local remote_path="${2:-}" | |
| print_message "INFO" "檢查 $host_alias 的掛載狀態" | |
| # 查找所有掛載點 | |
| local mount_info | |
| mount_info=$(find_mount_points_by_mount "$host_alias") | |
| if [[ -z "$mount_info" ]]; then | |
| print_message "INFO" "✗ 未掛載" | |
| return 0 | |
| fi | |
| # 如果指定了 remote_path,只顯示匹配的 | |
| if [[ -n "$remote_path" ]]; then | |
| local found=false | |
| while IFS='|' read -r existing_path mount_point; do | |
| if [[ "$existing_path" == "$remote_path" ]]; then | |
| found=true | |
| local full_mount_info | |
| full_mount_info=$(mount | grep " $mount_point ") | |
| print_message "INFO" "✓ 已掛載: $full_mount_info" | |
| # 檢查掛載點是否可訪問 | |
| if timeout 5 ls "$mount_point" >/dev/null 2>&1; then | |
| print_message "INFO" "✓ 掛載點可正常訪問" | |
| else | |
| print_message "WARN" "⚠ 掛載點無法訪問,可能需要重新掛載" | |
| fi | |
| fi | |
| done <<< "$mount_info" | |
| if [[ "$found" == false ]]; then | |
| print_message "INFO" "✗ 指定路徑 $remote_path 未掛載" | |
| fi | |
| else | |
| # 顯示所有掛載 | |
| print_message "INFO" "找到 $(echo "$mount_info" | wc -l) 個掛載:" | |
| while IFS='|' read -r existing_path mount_point; do | |
| local path_display="${existing_path:-~}" | |
| print_message "INFO" " [$path_display] → $mount_point" | |
| # 檢查掛載點是否可訪問 | |
| if timeout 2 ls "$mount_point" >/dev/null 2>&1; then | |
| print_message "INFO" " ✓ 可訪問" | |
| else | |
| print_message "WARN" " ✗ 無法訪問" | |
| fi | |
| done <<< "$mount_info" | |
| fi | |
| } | |
| # 顯示所有掛載中的rclone | |
| show_all_mounts() { | |
| print_message "INFO" "檢查所有rclone掛載..." | |
| echo | |
| # 獲取所有rclone類型的掛載 | |
| local mounts | |
| mounts=$(mount | grep "type fuse.rclone") | |
| if [[ -z "$mounts" ]]; then | |
| print_message "INFO" "目前沒有rclone掛載" | |
| return 0 | |
| fi | |
| echo -e "${GREEN}=== 已掛載的rclone遠端 ===${NC}" | |
| echo | |
| # 解析並顯示每個掛載 | |
| while IFS= read -r line; do | |
| # 提取remote名稱和掛載點 | |
| local remote | |
| remote=$(echo "$line" | awk '{print $1}' | cut -d: -f1) | |
| local mount_point | |
| mount_point=$(echo "$line" | awk '{print $3}') | |
| echo -e "${YELLOW}Remote:${NC} $remote" | |
| echo -e "${YELLOW}掛載點:${NC} $mount_point" | |
| # 測試掛載點訪問 | |
| if timeout 2 ls "$mount_point" >/dev/null 2>&1; then | |
| echo -e "${YELLOW}狀態:${NC} ${GREEN}✓ 可訪問${NC}" | |
| else | |
| echo -e "${YELLOW}狀態:${NC} ${RED}✗ 無法訪問${NC}" | |
| fi | |
| # 顯示掛載詳細資訊 | |
| echo -e "${YELLOW}詳細:${NC} $line" | |
| echo | |
| done <<< "$mounts" | |
| # 顯示相關的rclone進程 | |
| local rclone_pids | |
| rclone_pids=$(safe_ps | grep "rclone mount" | grep -v grep | grep -v "rclonemm") | |
| if [[ -n "$rclone_pids" ]]; then | |
| echo -e "${GREEN}=== rclone進程 ===${NC}" | |
| echo "$rclone_pids" | |
| fi | |
| } | |
| # 批量掛載所有可用的 SSH hosts | |
| mount_all_remotes() { | |
| local success_count=0 | |
| local fail_count=0 | |
| local failed_hosts=() | |
| print_message "INFO" "獲取所有可用的 SSH Host..." | |
| # 獲取所有可用的 hosts | |
| local all_hosts | |
| all_hosts=$(grep "^Host " "$SSH_CONFIG" | awk '{print $2}' | grep -v '\*' | grep -v 'github.com' | sort) | |
| if [[ -z "$all_hosts" ]]; then | |
| print_message "WARN" "沒有可掛載的 SSH Host" | |
| return 0 | |
| fi | |
| local total_hosts | |
| total_hosts=$(echo "$all_hosts" | wc -l) | |
| print_message "INFO" "找到 $total_hosts 個 SSH Host,開始批量掛載..." | |
| echo | |
| # 暫時禁用 errexit,避免單個失敗中斷整個流程 | |
| set +e | |
| local current=0 | |
| while IFS= read -r host; do | |
| [[ -z "$host" ]] && continue | |
| ((current++)) | |
| local mount_point="$MOUNT_BASE_DIR/$host" | |
| # 檢查是否已掛載 | |
| if is_mounted "$mount_point"; then | |
| print_message "INFO" "[$current/$total_hosts] $host 已經掛載在 $mount_point,跳過" | |
| ((success_count++)) | |
| continue | |
| fi | |
| print_message "INFO" "==== [$current/$total_hosts] 掛載 $host ====" | |
| # 使用核心掛載函數 | |
| if do_mount_single "$host" "" "$mount_point"; then | |
| ((success_count++)) | |
| print_message "INFO" "✓ $host 掛載成功" | |
| else | |
| ((fail_count++)) | |
| failed_hosts+=("$host") | |
| print_message "WARN" "✗ $host 掛載失敗,繼續下一個" | |
| fi | |
| echo | |
| done <<< "$all_hosts" | |
| # 恢復 errexit | |
| set -e | |
| # 顯示統計 | |
| echo | |
| print_message "INFO" "==== 批量掛載完成 ====" | |
| print_message "INFO" "成功: $success_count, 失敗: $fail_count, 總計: $total_hosts" | |
| if [[ $fail_count -gt 0 ]]; then | |
| print_message "WARN" "失敗的 hosts: ${failed_hosts[*]}" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # 批量卸載所有 rclone 掛載 | |
| unmount_all_remotes() { | |
| local success_count=0 | |
| local fail_count=0 | |
| local failed_mounts=() | |
| print_message "INFO" "檢查所有 rclone 掛載..." | |
| # 獲取所有 rclone 掛載 | |
| local all_mounts | |
| all_mounts=$(mount | grep "type fuse.rclone" || true) | |
| if [[ -z "$all_mounts" ]]; then | |
| print_message "INFO" "目前沒有 rclone 掛載" | |
| return 0 | |
| fi | |
| local total_mounts | |
| total_mounts=$(echo "$all_mounts" | wc -l) | |
| print_message "INFO" "找到 $total_mounts 個 rclone 掛載,開始批量卸載..." | |
| echo | |
| # 暫時禁用 errexit | |
| set +e | |
| local current=0 | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| ((current++)) | |
| local mount_point | |
| mount_point=$(echo "$line" | awk '{print $3}') | |
| local remote_part | |
| remote_part=$(echo "$line" | awk '{print $1}') | |
| # 提取 host alias(去除可能的參數和路徑) | |
| local host_alias | |
| if [[ "$remote_part" =~ ^([^{:]+) ]]; then | |
| host_alias="${BASH_REMATCH[1]}" | |
| else | |
| host_alias=$(echo "$remote_part" | cut -d: -f1) | |
| fi | |
| print_message "INFO" "==== [$current/$total_mounts] 卸載 $mount_point ====" | |
| # 使用核心卸載函數 perform_unmount | |
| if perform_unmount "$mount_point" "$host_alias"; then | |
| ((success_count++)) | |
| print_message "INFO" "✓ $mount_point 卸載成功" | |
| else | |
| ((fail_count++)) | |
| failed_mounts+=("$mount_point") | |
| print_message "WARN" "✗ $mount_point 卸載失敗,繼續下一個" | |
| fi | |
| echo | |
| done <<< "$all_mounts" | |
| # 恢復 errexit | |
| set -e | |
| # 顯示統計 | |
| echo | |
| print_message "INFO" "==== 批量卸載完成 ====" | |
| print_message "INFO" "成功: $success_count, 失敗: $fail_count, 總計: $total_mounts" | |
| if [[ $fail_count -gt 0 ]]; then | |
| print_message "WARN" "失敗的掛載點: ${failed_mounts[*]}" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # 主函數 | |
| main() { | |
| # 檢查依賴 | |
| check_dependencies | |
| # 清理過期日誌 | |
| cleanup_old_logs | |
| # 檢查參數 | |
| if [[ $# -eq 0 ]]; then | |
| print_message "ERROR" "缺少必要參數" | |
| show_help | |
| exit 1 | |
| fi | |
| local operation="$1" | |
| local remote_spec="${2:-}" | |
| local custom_mount_point="${3:-}" | |
| # 處理help操作 | |
| if [[ "$operation" == "help" || "$operation" == "-h" || "$operation" == "--help" ]]; then | |
| show_help | |
| exit 0 | |
| fi | |
| # 處理show操作(不需要remote名稱) | |
| if [[ "$operation" == "show" ]]; then | |
| show_all_mounts | |
| exit 0 | |
| fi | |
| # 處理批量操作(--all 或 -a) | |
| if [[ "$remote_spec" == "--all" || "$remote_spec" == "-a" ]]; then | |
| # 確保日誌目錄存在 | |
| sudo mkdir -p "$LOG_DIR" | |
| print_message "INFO" "批量操作: $operation 所有 remotes,日誌: $LOG_FILE" | |
| case "$operation" in | |
| "mount"|"mnt") | |
| mount_all_remotes | |
| exit $? | |
| ;; | |
| "unmount"|"umount"|"umnt") | |
| unmount_all_remotes | |
| exit $? | |
| ;; | |
| *) | |
| print_message "ERROR" "操作 '$operation' 不支援 --all/-a 參數" | |
| print_message "INFO" "支援 --all/-a 的操作: mount, unmount" | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| # 檢查是否提供remote規格 | |
| if [[ -z "$remote_spec" ]]; then | |
| print_message "ERROR" "缺少 SSH Host 名稱參數或 --all/-a 選項" | |
| show_help | |
| exit 1 | |
| fi | |
| # 解析 host alias 和路徑 | |
| # 格式: host_alias 或 host_alias:path | |
| local host_alias remote_path | |
| if [[ "$remote_spec" =~ ^([^:]+):(.*)$ ]]; then | |
| host_alias="${BASH_REMATCH[1]}" | |
| remote_path="${BASH_REMATCH[2]}" | |
| else | |
| host_alias="$remote_spec" | |
| remote_path="" | |
| fi | |
| # 檢查 SSH Host 是否存在 | |
| if ! check_remote_exists "$host_alias"; then | |
| exit 1 | |
| fi | |
| # 確保日誌目錄存在 | |
| sudo mkdir -p "$LOG_DIR" | |
| # 執行對應操作 | |
| print_message "INFO" "操作: $operation,日誌: $LOG_FILE" | |
| case "$operation" in | |
| "mount"|"mnt") | |
| mount_remote "$host_alias" "$remote_path" "$custom_mount_point" | |
| ;; | |
| "unmount"|"umount"|"umnt") | |
| unmount_remote "$host_alias" "$custom_mount_point" | |
| ;; | |
| "status") | |
| check_status "$host_alias" "$remote_path" | |
| ;; | |
| *) | |
| print_message "ERROR" "未知操作: $operation" | |
| show_help | |
| exit 1 | |
| ;; | |
| esac | |
| } | |
| # 執行主函數 | |
| main "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| rclone Mount Management Tool (PowerShell Version) | |
| .DESCRIPTION | |
| Features: mount, unmount, status, show, help | |
| Uses SSH config to mount rclone SFTP remote to local directory | |
| .PARAMETER Operation | |
| Operation type: mount/mnt, unmount/umount/umnt, status, show, help | |
| .PARAMETER RemoteSpec | |
| Host name from SSH config, format: <remote_name>[:<remote_path>] | |
| Or use --all/--a for batch operations | |
| .PARAMETER CustomMountPoint | |
| Custom local mount point (optional) | |
| .EXAMPLE | |
| .\rclonemm_en.ps1 mount mom | |
| Mount mom root directory to default location | |
| .EXAMPLE | |
| .\rclonemm_en.ps1 mount mom:/WEB/logs | |
| Mount mom specified directory to default location | |
| .EXAMPLE | |
| .\rclonemm_en.ps1 mount mom D:\custom\path | |
| Mount mom root directory to custom location | |
| .EXAMPLE | |
| .\rclonemm_en.ps1 mount --all | |
| Batch mount all SSH Hosts | |
| .EXAMPLE | |
| .\rclonemm_en.ps1 unmount mom | |
| Unmount mom | |
| .EXAMPLE | |
| .\rclonemm_en.ps1 status mom | |
| Check mom status | |
| .NOTES | |
| Version: 1.0.0 | |
| Date: 2026-01-02 | |
| Author: Translated from Bash version by GitHub Copilot | |
| Original Bash version update: 2025-12-19 | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Position = 0)] | |
| [string]$Operation, | |
| [Parameter(Position = 1)] | |
| [string]$RemoteSpec, | |
| [Parameter(Position = 2)] | |
| [string]$CustomMountPoint | |
| ) | |
| # ==================== Error Handling Settings ==================== | |
| $ErrorActionPreference = 'Stop' | |
| Set-StrictMode -Version Latest | |
| # ==================== Global Configuration ==================== | |
| $script:Config = @{ | |
| SSHConfig = "$env:USERPROFILE\.ssh\config" | |
| MountBaseDir = "C:\mnt" | |
| LogDir = "$env:LOCALAPPDATA\rclonemm\logs" | |
| LogRetentionDays = 7 | |
| MountTimeout = 10 | |
| AccessTestTimeout = 10 | |
| } | |
| # Support custom PREFIX environment variable | |
| if ($env:RCLONEMM_PREFIX) { | |
| Write-Verbose "Using custom PREFIX: $env:RCLONEMM_PREFIX" | |
| $script:Config.MountBaseDir = "$env:RCLONEMM_PREFIX\mnt" | |
| $script:Config.LogDir = "$env:RCLONEMM_PREFIX\var\log" | |
| } | |
| # Log file path | |
| $script:LogFile = Join-Path $script:Config.LogDir "rclone_mount_$(Get-Date -Format 'yyyyMMdd').log" | |
| # ==================== Helper Functions: Logging & Messages ==================== | |
| <# | |
| .SYNOPSIS | |
| Output colored message and log to file | |
| #> | |
| function Write-ColorMessage { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')] | |
| [string]$Level, | |
| [Parameter(Mandatory)] | |
| [string]$Message | |
| ) | |
| $color = switch ($Level) { | |
| 'INFO' { 'Green' } | |
| 'WARN' { 'Yellow' } | |
| 'ERROR' { 'Red' } | |
| 'DEBUG' { 'Cyan' } | |
| } | |
| Write-Host "[$Level] $Message" -ForegroundColor $color | |
| # Log to file | |
| Write-Log -Level $Level -Message $Message | |
| } | |
| <# | |
| .SYNOPSIS | |
| Write to log file | |
| #> | |
| function Write-Log { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$Level, | |
| [Parameter(Mandatory)] | |
| [string]$Message | |
| ) | |
| try { | |
| $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' | |
| $logEntry = "[$timestamp] [$Level] $Message" | |
| # Ensure log directory exists | |
| $logDir = Split-Path $script:LogFile -Parent | |
| if (-not (Test-Path $logDir)) { | |
| New-Item -Path $logDir -ItemType Directory -Force | Out-Null | |
| } | |
| # Write log | |
| Add-Content -Path $script:LogFile -Value $logEntry -ErrorAction SilentlyContinue | |
| } catch { | |
| # Ignore log write errors | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Clean up old log files | |
| #> | |
| function Clear-OldLogs { | |
| [CmdletBinding()] | |
| param() | |
| try { | |
| $logDir = $script:Config.LogDir | |
| if (-not (Test-Path $logDir)) { | |
| return | |
| } | |
| $cutoffDate = (Get-Date).AddDays(-$script:Config.LogRetentionDays) | |
| Get-ChildItem -Path $logDir -Filter "rclone_mount_*.log" | | |
| Where-Object { $_.LastWriteTime -lt $cutoffDate } | | |
| Remove-Item -Force -ErrorAction SilentlyContinue | |
| } catch { | |
| # Ignore cleanup errors | |
| } | |
| } | |
| # ==================== Helper Functions: Dependency Check ==================== | |
| <# | |
| .SYNOPSIS | |
| Check required dependencies | |
| #> | |
| function Test-Dependencies { | |
| [CmdletBinding()] | |
| param() | |
| $missingDeps = @() | |
| # Check rclone | |
| if (-not (Get-Command rclone -ErrorAction SilentlyContinue)) { | |
| $missingDeps += "rclone" | |
| } | |
| # Check WinFsp (via registry or filesystem) | |
| $winfspPaths = @( | |
| "$env:ProgramFiles\WinFsp", | |
| "${env:ProgramFiles(x86)}\WinFsp" | |
| ) | |
| $winfspInstalled = $winfspPaths | Where-Object { Test-Path $_ } | |
| if (-not $winfspInstalled) { | |
| $missingDeps += "WinFsp" | |
| } | |
| # If there are missing dependencies, show error and exit | |
| if ($missingDeps.Count -gt 0) { | |
| Write-ColorMessage -Level ERROR -Message "Missing required dependencies:" | |
| foreach ($dep in $missingDeps) { | |
| Write-Host " x $dep" -ForegroundColor Red | |
| } | |
| Write-Host "" | |
| Write-Host "Please install the missing packages:" -ForegroundColor Yellow | |
| Write-Host " rclone: https://rclone.org/downloads/" -ForegroundColor Yellow | |
| Write-Host " WinFsp: https://winfsp.dev/" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Check if running with administrator privileges | |
| #> | |
| function Test-AdminPrivileges { | |
| [CmdletBinding()] | |
| param() | |
| $identity = [Security.Principal.WindowsIdentity]::GetCurrent() | |
| $principal = [Security.Principal.WindowsPrincipal]$identity | |
| return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | |
| } | |
| # ==================== Helper Functions: SSH Config Parsing ==================== | |
| <# | |
| .SYNOPSIS | |
| Parse SSH config to get specified property for a Host | |
| #> | |
| function Get-SSHConfigValue { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter(Mandatory)] | |
| [string]$Key | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return $null | |
| } | |
| $content = Get-Content $sshConfigPath -Raw | |
| $lines = $content -split "`r?`n" | |
| $inHostBlock = $false | |
| foreach ($line in $lines) { | |
| # Check if entering target Host block | |
| if ($line -match '^\s*Host\s+(.+)$') { | |
| $hostPatterns = $Matches[1] -split '\s+' | |
| $inHostBlock = $hostPatterns -contains $HostAlias | |
| continue | |
| } | |
| # If in target Host block, find property | |
| if ($inHostBlock) { | |
| if ($line -match "^\s*$Key\s+(.+)$") { | |
| return $Matches[1].Trim() | |
| } | |
| # Reached next Host block, stop searching | |
| if ($line -match '^\s*Host\s+') { | |
| break | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| <# | |
| .SYNOPSIS | |
| Get complete SSH Host configuration | |
| #> | |
| function Get-SSHHostConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $hostname = Get-SSHConfigValue -HostAlias $HostAlias -Key 'HostName' | |
| $user = Get-SSHConfigValue -HostAlias $HostAlias -Key 'User' | |
| $port = Get-SSHConfigValue -HostAlias $HostAlias -Key 'Port' | |
| $identityFile = Get-SSHConfigValue -HostAlias $HostAlias -Key 'IdentityFile' | |
| $proxyJump = Get-SSHConfigValue -HostAlias $HostAlias -Key 'ProxyJump' | |
| # If no explicit HostName, use Host alias | |
| if (-not $hostname) { | |
| $hostname = $HostAlias | |
| } | |
| # Expand ~ to actual path | |
| if ($identityFile -and $identityFile.StartsWith('~')) { | |
| $identityFile = $identityFile -replace '^~', $env:USERPROFILE | |
| } | |
| return [PSCustomObject]@{ | |
| HostAlias = $HostAlias | |
| HostName = $hostname | |
| User = $user | |
| Port = $port | |
| IdentityFile = $identityFile | |
| ProxyJump = $proxyJump | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Build --sftp-ssh option | |
| #> | |
| function Get-SftpSshOption { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [PSCustomObject]$Config | |
| ) | |
| $sshCmd = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=2" | |
| # Add Port | |
| if ($Config.Port) { | |
| $sshCmd += " -p $($Config.Port)" | |
| } | |
| # Add IdentityFile | |
| if ($Config.IdentityFile) { | |
| $sshCmd += " -i `"$($Config.IdentityFile)`"" | |
| } | |
| # Add ProxyJump | |
| if ($Config.ProxyJump) { | |
| $sshCmd += " -J $($Config.ProxyJump)" | |
| } | |
| # Build complete SSH connection string user@hostname | |
| $sshTarget = if ($Config.User) { | |
| "$($Config.User)@$($Config.HostName)" | |
| } else { | |
| $Config.HostName | |
| } | |
| # Return complete SSH command (without outer quotes, as it will be passed as parameter) | |
| return "$sshCmd $sshTarget" | |
| } | |
| # ==================== Helper Functions: Mount Point Management ==================== | |
| <# | |
| .SYNOPSIS | |
| Check if specified path is mounted | |
| #> | |
| function Test-Mounted { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint | |
| ) | |
| # Find rclone processes via Get-Process and check command line | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| return $false | |
| } | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| if ($cmdLine -like "*mount*" -and $cmdLine -like "*$MountPoint*") { | |
| return $true | |
| } | |
| } catch { | |
| # Ignore permission errors | |
| } | |
| } | |
| return $false | |
| } | |
| <# | |
| .SYNOPSIS | |
| Ensure parent directory of mount point exists, but mount point itself must not exist | |
| On Windows, rclone requires mount point to not be an existing directory | |
| #> | |
| function Ensure-MountPoint { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint | |
| ) | |
| # Check if mount point already exists | |
| if (Test-Path $MountPoint) { | |
| Write-ColorMessage -Level ERROR -Message "Mount point already exists: $MountPoint (On Windows, rclone requires mount point to not exist)" | |
| return $false | |
| } | |
| # Check if parent directory exists | |
| $parentPath = Split-Path $MountPoint -Parent | |
| if (-not $parentPath) { | |
| Write-ColorMessage -Level ERROR -Message "Invalid mount point path: $MountPoint" | |
| return $false | |
| } | |
| if (-not (Test-Path $parentPath)) { | |
| Write-ColorMessage -Level INFO -Message "Creating parent directory: $parentPath" | |
| try { | |
| New-Item -Path $parentPath -ItemType Directory -Force | Out-Null | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "Cannot create parent directory: $parentPath - $_" | |
| return $false | |
| } | |
| } | |
| Write-ColorMessage -Level INFO -Message "Mount point path verified: $MountPoint" | |
| return $true | |
| } | |
| <# | |
| .SYNOPSIS | |
| Find all mount points for specified host alias | |
| Return format: @{RemotePath=''; MountPoint=''} | |
| #> | |
| function Find-MountPoints { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $mountPoints = @() | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| return ,$mountPoints | |
| } | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| # Check if it's a mount operation | |
| if ($cmdLine -notlike "*mount*") { | |
| continue | |
| } | |
| # Parse command line, extract remote source and mount point | |
| # Format: rclone mount "remote:path" "mount_point" [options] | |
| # Or: rclone mount remote:path mount_point [options] | |
| $pattern = 'mount\s+"?([^":]+):([^"]*)"?\s+"?([^"]+)"?' | |
| if ($cmdLine -match $pattern) { | |
| $remote = $Matches[1] | |
| $remotePath = $Matches[2] | |
| $mountPoint = $Matches[3] | |
| if ($remote -eq $HostAlias) { | |
| $mountPoints += [PSCustomObject]@{ | |
| RemotePath = $remotePath | |
| MountPoint = $mountPoint | |
| ProcessId = $process.Id | |
| } | |
| } | |
| } | |
| } catch { | |
| # Ignore errors | |
| } | |
| } | |
| return ,$mountPoints | |
| } | |
| <# | |
| .SYNOPSIS | |
| Find rclone process PIDs matching host alias and mount point | |
| #> | |
| function Find-RclonePids { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$MountPoint | |
| ) | |
| $pids = @() | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| return ,$pids | |
| } | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| # Check if matches host alias | |
| if ($cmdLine -notlike "*$HostAlias*") { | |
| continue | |
| } | |
| # If mount point specified, also match mount point | |
| if ($MountPoint -and $cmdLine -notlike "*$MountPoint*") { | |
| continue | |
| } | |
| $pids += $process.Id | |
| } catch { | |
| # Ignore errors | |
| } | |
| } | |
| return ,$pids | |
| } | |
| # ==================== Core Functions: Mount Operations ==================== | |
| <# | |
| .SYNOPSIS | |
| Core mount function (single host mount logic) | |
| #> | |
| function Invoke-MountSingle { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$RemotePath = '', | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint | |
| ) | |
| # Get SSH Host configuration | |
| $sshConfig = Get-SSHHostConfig -HostAlias $HostAlias | |
| # Build --sftp-ssh option | |
| $sftpSshOption = Get-SftpSshOption -Config $sshConfig | |
| # Combine remote source path | |
| $remoteSource = if ($RemotePath) { | |
| "${HostAlias}:$RemotePath" | |
| } else { | |
| "${HostAlias}:" | |
| } | |
| # Check if target mount point is occupied | |
| if (Test-Mounted -MountPoint $MountPoint) { | |
| Write-ColorMessage -Level ERROR -Message "Mount point $MountPoint is already occupied by another filesystem" | |
| return $false | |
| } | |
| # Ensure mount point path is valid (parent directory exists, but mount point itself does not) | |
| if (-not (Ensure-MountPoint -MountPoint $MountPoint)) { | |
| return $false | |
| } | |
| # Create rclone config before mounting | |
| Write-ColorMessage -Level INFO -Message "Creating rclone config: $HostAlias" | |
| try { | |
| $configResult = & rclone config create $HostAlias sftp 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $configResult" | |
| return $false | |
| } | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $_" | |
| return $false | |
| } | |
| # Build rclone arguments | |
| $argString = "mount `"$remoteSource`" `"$MountPoint`" --vfs-cache-mode full --default-permissions --multi-thread-streams 4 --multi-thread-cutoff 100M --low-level-retries 2 --sftp-disable-hashcheck --sftp-ssh `"$sftpSshOption`"" | |
| Write-ColorMessage -Level INFO -Message "Mount command: rclone $argString" | |
| Write-ColorMessage -Level INFO -Message "Tip: If SSH requires password, enter it when prompted" | |
| # Execute mount command (run in background directly) | |
| try { | |
| # Use Start-Process and pass arguments directly | |
| # Pass entire command string, let PowerShell handle quotes automatically | |
| $process = Start-Process -FilePath "rclone" -ArgumentList $argString -NoNewWindow -PassThru | |
| # Wait for mount to complete | |
| $count = 0 | |
| while ($count -lt $script:Config.MountTimeout) { | |
| Write-ColorMessage -Level INFO -Message "Waiting for mount... ($($count + 1)/$($script:Config.MountTimeout))" | |
| Start-Sleep -Seconds 1 | |
| if (Test-Mounted -MountPoint $MountPoint) { | |
| Write-ColorMessage -Level INFO -Message "Mount process started: ${HostAlias}:$RemotePath -> $MountPoint" | |
| # Wait for rclone service to fully start | |
| # Test if mount point is accessible, retry a few times | |
| Write-ColorMessage -Level INFO -Message "Waiting for rclone service to fully start..." | |
| $accessTestCount = 0 | |
| $maxAccessTests = 5 | |
| $accessSuccess = $false | |
| while ($accessTestCount -lt $maxAccessTests) { | |
| Start-Sleep -Seconds 2 | |
| $accessTestCount++ | |
| Write-ColorMessage -Level INFO -Message "Testing mount point access... (attempt $accessTestCount/$maxAccessTests)" | |
| try { | |
| # Try to access mount point | |
| if (Test-Path $MountPoint) { | |
| $testResult = Get-ChildItem -Path $MountPoint -ErrorAction Stop | Select-Object -First 1 | |
| Write-ColorMessage -Level INFO -Message "Mount point test passed, log: $($script:LogFile)" | |
| $accessSuccess = $true | |
| break | |
| } | |
| } catch { | |
| Write-ColorMessage -Level DEBUG -Message "Access test failed (attempt $accessTestCount/$maxAccessTests): $_" | |
| # Continue retry | |
| } | |
| } | |
| if ($accessSuccess) { | |
| # Clear rclone config after successful mount | |
| Write-ColorMessage -Level INFO -Message "Clearing rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $true | |
| } else { | |
| Write-ColorMessage -Level ERROR -Message "Mount point still inaccessible after $maxAccessTests attempts, mount failed" | |
| Write-ColorMessage -Level WARN -Message "Performing cleanup..." | |
| # Perform cleanup | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias -MountPoint $MountPoint | |
| foreach ($procId in $rclonePids) { | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| # Clear rclone config | |
| Write-ColorMessage -Level INFO -Message "Clearing rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $false | |
| } | |
| } | |
| $count++ | |
| } | |
| Write-ColorMessage -Level ERROR -Message "Mount timeout, please check log: $($script:LogFile)" | |
| Write-ColorMessage -Level WARN -Message "Performing cleanup..." | |
| # Perform cleanup | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias -MountPoint $MountPoint | |
| foreach ($procId in $rclonePids) { | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| # Clear rclone config | |
| Write-ColorMessage -Level INFO -Message "Clearing rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $false | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "rclone mount command failed: $_" | |
| Write-ColorMessage -Level WARN -Message "Performing cleanup..." | |
| # Clear rclone config | |
| Write-ColorMessage -Level INFO -Message "Clearing rclone config: $HostAlias" | |
| & rclone config delete $HostAlias 2>&1 | Out-Null | |
| return $false | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Mount remote (user interface function) | |
| #> | |
| function Mount-RcloneRemote { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$RemotePath = '', | |
| [Parameter()] | |
| [string]$CustomMountPoint = '' | |
| ) | |
| # Determine mount point: custom > default | |
| $mountPoint = if ($CustomMountPoint) { | |
| $CustomMountPoint | |
| } else { | |
| Join-Path $script:Config.MountBaseDir $HostAlias | |
| } | |
| Write-ColorMessage -Level INFO -Message "Using --sftp-ssh option for mounting" | |
| Write-ColorMessage -Level INFO -Message "Mounting ${HostAlias}:$RemotePath to $mountPoint" | |
| if ($CustomMountPoint) { | |
| Write-ColorMessage -Level INFO -Message "Using custom mount point: $CustomMountPoint" | |
| } | |
| # Call core mount function | |
| return Invoke-MountSingle -HostAlias $HostAlias -RemotePath $RemotePath -MountPoint $mountPoint | |
| } | |
| # ==================== Core Functions: Unmount Operations ==================== | |
| <# | |
| .SYNOPSIS | |
| Perform actual unmount operation | |
| #> | |
| function Invoke-Unmount { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$MountPoint, | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| Write-ColorMessage -Level INFO -Message "Unmounting $MountPoint" | |
| # Find related rclone processes | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias -MountPoint $MountPoint | |
| if ($rclonePids.Count -eq 0) { | |
| Write-ColorMessage -Level WARN -Message "No related rclone processes found" | |
| return $false | |
| } | |
| Write-ColorMessage -Level INFO -Message "Found related rclone processes: $($rclonePids -join ', ')" | |
| Write-ColorMessage -Level INFO -Message "Terminating processes..." | |
| # Try normal termination first | |
| foreach ($procId in $rclonePids) { | |
| try { | |
| Stop-Process -Id $procId -ErrorAction Stop | |
| } catch { | |
| Write-ColorMessage -Level WARN -Message "Cannot terminate process $procId, trying force termination" | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| Start-Sleep -Seconds 2 | |
| Write-ColorMessage -Level INFO -Message "rclone processes terminated" | |
| # On Windows, do not delete mount point directory after unmount | |
| # Because rclone requires path to not exist before mount, should not auto-delete after unmount | |
| # If directory exists, it may have other uses or leftover files | |
| if (Test-Path $MountPoint) { | |
| Write-ColorMessage -Level INFO -Message "Mount point directory still exists: $MountPoint (not auto-deleted)" | |
| } else { | |
| Write-ColorMessage -Level INFO -Message "Mount point cleared: $MountPoint" | |
| } | |
| return $true | |
| } | |
| <# | |
| .SYNOPSIS | |
| Unmount remote | |
| #> | |
| function Dismount-RcloneRemote { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$CustomMountPoint = '' | |
| ) | |
| # Find all mount points | |
| $mountInfo = Find-MountPoints -HostAlias $HostAlias | |
| if ($mountInfo.Count -eq 0) { | |
| Write-ColorMessage -Level WARN -Message "$HostAlias is not mounted" | |
| # Check if there are still related rclone processes running | |
| $rclonePids = Find-RclonePids -HostAlias $HostAlias | |
| if ($rclonePids.Count -gt 0) { | |
| Write-ColorMessage -Level INFO -Message "Found residual rclone processes: $($rclonePids -join ', ')" | |
| Write-ColorMessage -Level INFO -Message "Terminating processes..." | |
| foreach ($procId in $rclonePids) { | |
| Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue | |
| } | |
| Start-Sleep -Seconds 2 | |
| Write-ColorMessage -Level INFO -Message "rclone processes terminated" | |
| } | |
| return $true | |
| } | |
| # If mount point specified, only unmount matching ones | |
| if ($CustomMountPoint) { | |
| $found = $false | |
| foreach ($mount in $mountInfo) { | |
| if ($mount.MountPoint -eq $CustomMountPoint) { | |
| $found = $true | |
| Invoke-Unmount -MountPoint $mount.MountPoint -HostAlias $HostAlias | |
| break | |
| } | |
| } | |
| if (-not $found) { | |
| Write-ColorMessage -Level ERROR -Message "Cannot find mounted path: $HostAlias $CustomMountPoint" | |
| Write-ColorMessage -Level INFO -Message "Current $HostAlias mounts:" | |
| foreach ($mount in $mountInfo) { | |
| $pathDisplay = if ($mount.RemotePath) { $mount.RemotePath } else { "~" } | |
| Write-ColorMessage -Level INFO -Message " [$pathDisplay] -> $($mount.MountPoint)" | |
| } | |
| return $false | |
| } | |
| } else { | |
| # No mount point specified, ask for confirmation one by one | |
| Write-ColorMessage -Level INFO -Message "Found $($mountInfo.Count) mount(s) for ${HostAlias}:" | |
| foreach ($mount in $mountInfo) { | |
| $pathDisplay = if ($mount.RemotePath) { $mount.RemotePath } else { "~" } | |
| Write-Host "" | |
| Write-ColorMessage -Level INFO -Message "Mount: [$pathDisplay] on $($mount.MountPoint)" | |
| $answer = Read-Host "Do you want to unmount this? [y/N]" | |
| if ($answer -match '^[Yy]$') { | |
| Invoke-Unmount -MountPoint $mount.MountPoint -HostAlias $HostAlias | |
| } else { | |
| Write-ColorMessage -Level INFO -Message "Skipping $($mount.MountPoint)" | |
| } | |
| } | |
| } | |
| return $true | |
| } | |
| # ==================== Core Functions: Status Check ==================== | |
| <# | |
| .SYNOPSIS | |
| Check mount status | |
| #> | |
| function Get-RcloneStatus { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter()] | |
| [string]$RemotePath = '' | |
| ) | |
| Write-ColorMessage -Level INFO -Message "Checking mount status for $HostAlias" | |
| # Find all mount points | |
| $mountInfo = Find-MountPoints -HostAlias $HostAlias | |
| if ($mountInfo.Count -eq 0) { | |
| Write-ColorMessage -Level INFO -Message "Not mounted" | |
| return | |
| } | |
| # If RemotePath specified, only show matching ones | |
| if ($RemotePath) { | |
| $found = $false | |
| foreach ($mount in $mountInfo) { | |
| if ($mount.RemotePath -eq $RemotePath) { | |
| $found = $true | |
| Write-ColorMessage -Level INFO -Message "Mounted: $($mount.MountPoint)" | |
| # Check if mount point is accessible | |
| try { | |
| $null = Get-ChildItem -Path $mount.MountPoint -ErrorAction Stop | |
| Write-ColorMessage -Level INFO -Message "Mount point accessible" | |
| } catch { | |
| Write-ColorMessage -Level WARN -Message "Mount point inaccessible, may need remounting" | |
| } | |
| } | |
| } | |
| if (-not $found) { | |
| Write-ColorMessage -Level INFO -Message "Specified path $RemotePath is not mounted" | |
| } | |
| } else { | |
| # Show all mounts | |
| Write-ColorMessage -Level INFO -Message "Found $($mountInfo.Count) mount(s):" | |
| foreach ($mount in $mountInfo) { | |
| $pathDisplay = if ($mount.RemotePath) { $mount.RemotePath } else { "~" } | |
| Write-ColorMessage -Level INFO -Message " [$pathDisplay] -> $($mount.MountPoint)" | |
| # Check if mount point is accessible | |
| try { | |
| $null = Get-ChildItem -Path $mount.MountPoint -ErrorAction Stop | |
| Write-ColorMessage -Level INFO -Message " Accessible" | |
| } catch { | |
| Write-ColorMessage -Level WARN -Message " Inaccessible" | |
| } | |
| } | |
| } | |
| } | |
| # ==================== Helper Functions: Batch Operations ==================== | |
| <# | |
| .SYNOPSIS | |
| Batch mount all available SSH hosts | |
| #> | |
| function Mount-AllRemotes { | |
| [CmdletBinding()] | |
| param() | |
| $successCount = 0 | |
| $failCount = 0 | |
| $failedHosts = @() | |
| Write-ColorMessage -Level INFO -Message "Getting all available SSH Hosts..." | |
| # Get all available hosts | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| Write-ColorMessage -Level WARN -Message "SSH config does not exist: $sshConfigPath" | |
| return | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $allHosts = $content | Where-Object { $_ -match '^\s*Host\s+(.+)$' } | ForEach-Object { | |
| $Matches[1] -split '\s+' | Where-Object { $_ -notmatch '\*' -and $_ -ne 'github.com' } | |
| } | Select-Object -Unique | Sort-Object | |
| if ($allHosts.Count -eq 0) { | |
| Write-ColorMessage -Level WARN -Message "No SSH Hosts available for mounting" | |
| return | |
| } | |
| $totalHosts = $allHosts.Count | |
| Write-ColorMessage -Level INFO -Message "Found $totalHosts SSH Hosts, starting batch mount..." | |
| Write-Host "" | |
| $current = 0 | |
| foreach ($sshHost in $allHosts) { | |
| $current++ | |
| $mountPoint = Join-Path $script:Config.MountBaseDir $sshHost | |
| # Check if already mounted | |
| if (Test-Mounted -MountPoint $mountPoint) { | |
| Write-ColorMessage -Level INFO -Message "[$current/$totalHosts] $sshHost already mounted at $mountPoint, skipping" | |
| $successCount++ | |
| continue | |
| } | |
| Write-ColorMessage -Level INFO -Message "==== [$current/$totalHosts] Mounting $sshHost ====" | |
| # Use core mount function | |
| if (Invoke-MountSingle -HostAlias $sshHost -RemotePath '' -MountPoint $mountPoint) { | |
| $successCount++ | |
| Write-ColorMessage -Level INFO -Message "$sshHost mount successful" | |
| } else { | |
| $failCount++ | |
| $failedHosts += $sshHost | |
| Write-ColorMessage -Level WARN -Message "$sshHost mount failed, continuing to next" | |
| } | |
| Write-Host "" | |
| } | |
| # Show statistics | |
| Write-Host "" | |
| Write-ColorMessage -Level INFO -Message "==== Batch mount completed ====" | |
| Write-ColorMessage -Level INFO -Message "Success: $successCount, Failed: $failCount, Total: $totalHosts" | |
| if ($failCount -gt 0) { | |
| Write-ColorMessage -Level WARN -Message "Failed hosts: $($failedHosts -join ', ')" | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Batch unmount all rclone mounts | |
| #> | |
| function Dismount-AllRemotes { | |
| [CmdletBinding()] | |
| param() | |
| $successCount = 0 | |
| $failCount = 0 | |
| $failedMounts = @() | |
| Write-ColorMessage -Level INFO -Message "Checking all rclone mounts..." | |
| # Get all rclone processes | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| Write-ColorMessage -Level INFO -Message "No rclone mounts currently" | |
| return | |
| } | |
| $allMounts = @() | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| # Use -match with variable to avoid parsing issues | |
| $pattern = 'mount\s+"?([^":]+):([^"]*)"?\s+"?([^"]+)"?' | |
| if ($cmdLine -match $pattern) { | |
| $allMounts += [PSCustomObject]@{ | |
| HostAlias = $Matches[1] | |
| RemotePath = $Matches[2] | |
| MountPoint = $Matches[3] | |
| ProcessId = $process.Id | |
| } | |
| } | |
| } catch { | |
| # Ignore errors | |
| } | |
| } | |
| if ($allMounts.Count -eq 0) { | |
| Write-ColorMessage -Level INFO -Message "No rclone mounts currently" | |
| return | |
| } | |
| $totalMounts = $allMounts.Count | |
| Write-ColorMessage -Level INFO -Message "Found $totalMounts rclone mounts, starting batch unmount..." | |
| Write-Host "" | |
| $current = 0 | |
| foreach ($mount in $allMounts) { | |
| $current++ | |
| Write-ColorMessage -Level INFO -Message "==== [$current/$totalMounts] Unmounting $($mount.MountPoint) ====" | |
| # Use core unmount function | |
| if (Invoke-Unmount -MountPoint $mount.MountPoint -HostAlias $mount.HostAlias) { | |
| $successCount++ | |
| Write-ColorMessage -Level INFO -Message "$($mount.MountPoint) unmount successful" | |
| } else { | |
| $failCount++ | |
| $failedMounts += $mount.MountPoint | |
| Write-ColorMessage -Level WARN -Message "$($mount.MountPoint) unmount failed, continuing to next" | |
| } | |
| Write-Host "" | |
| } | |
| # Show statistics | |
| Write-Host "" | |
| Write-ColorMessage -Level INFO -Message "==== Batch unmount completed ====" | |
| Write-ColorMessage -Level INFO -Message "Success: $successCount, Failed: $failCount, Total: $totalMounts" | |
| if ($failCount -gt 0) { | |
| Write-ColorMessage -Level WARN -Message "Failed mount points: $($failedMounts -join ', ')" | |
| } | |
| } | |
| # ==================== Helper Functions: Other Features ==================== | |
| <# | |
| .SYNOPSIS | |
| Show all mounted rclone | |
| #> | |
| function Show-RcloneMounts { | |
| [CmdletBinding()] | |
| param() | |
| Write-ColorMessage -Level INFO -Message "Checking all rclone mounts..." | |
| Write-Host "" | |
| $rcloneProcesses = Get-Process -Name rclone -ErrorAction SilentlyContinue | |
| if (-not $rcloneProcesses) { | |
| Write-ColorMessage -Level INFO -Message "No rclone mounts currently" | |
| return | |
| } | |
| Write-Host "=== Mounted rclone remotes ===" -ForegroundColor Green | |
| Write-Host "" | |
| foreach ($process in $rcloneProcesses) { | |
| try { | |
| $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine | |
| $pattern = 'mount\s+"?([^":]+):([^"]*)"?\s+"?([^"]+)"?' | |
| if ($cmdLine -match $pattern) { | |
| $remote = $Matches[1] | |
| $remotePath = $Matches[2] | |
| $mountPoint = $Matches[3] | |
| Write-Host "Remote: " -ForegroundColor Yellow -NoNewline | |
| Write-Host $remote | |
| Write-Host "Path: " -ForegroundColor Yellow -NoNewline | |
| Write-Host $(if ($remotePath) { $remotePath } else { "~" }) | |
| Write-Host "Mount point: " -ForegroundColor Yellow -NoNewline | |
| Write-Host $mountPoint | |
| # Test mount point access | |
| try { | |
| $null = Get-ChildItem -Path $mountPoint -ErrorAction Stop | |
| Write-Host "Status: " -ForegroundColor Yellow -NoNewline | |
| Write-Host "Accessible" -ForegroundColor Green | |
| } catch { | |
| Write-Host "Status: " -ForegroundColor Yellow -NoNewline | |
| Write-Host "Inaccessible" -ForegroundColor Red | |
| } | |
| Write-Host "" | |
| } | |
| } catch { | |
| # Ignore errors | |
| } | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Get available SSH Host list | |
| #> | |
| function Get-AvailableRemotes { | |
| [CmdletBinding()] | |
| param() | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $sshHosts = $content | Where-Object { $_ -match '^\s*Host\s+(.+)$' } | ForEach-Object { | |
| $Matches[1] -split '\s+' | Where-Object { $_ -notmatch '\*' -and $_ -ne 'github.com' } | |
| } | Select-Object -Unique | Sort-Object | |
| foreach ($sshHost in $sshHosts) { | |
| # Check if this host is already mounted | |
| $mountInfo = Find-MountPoints -HostAlias $sshHost | |
| if ($mountInfo.Count -gt 0) { | |
| $firstMount = $mountInfo[0].MountPoint | |
| if ($mountInfo.Count -gt 1) { | |
| $status = "mounted {0}: {1} ..." -f $mountInfo.Count, $firstMount | |
| Write-Host "$sshHost [$status]" -ForegroundColor Yellow | |
| } else { | |
| $status = "mounted: {0}" -f $firstMount | |
| Write-Host "$sshHost [$status]" -ForegroundColor Yellow | |
| } | |
| } else { | |
| Write-Host $sshHost | |
| } | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Show usage help | |
| #> | |
| function Show-Help { | |
| [CmdletBinding()] | |
| param() | |
| Write-Host "rclone Mount Management Tool" -ForegroundColor Cyan | |
| Write-Host "" | |
| Write-Host "Usage:" -ForegroundColor Yellow | |
| Write-Host ' .\rclonemm_en.ps1 <operation> <remote_name>[:<remote_path>] [custom_mount_point]' | |
| Write-Host ' .\rclonemm_en.ps1 <operation> --all|--a' | |
| Write-Host "" | |
| Write-Host "Parameters:" -ForegroundColor Yellow | |
| Write-Host " operation Operation type" | |
| Write-Host " - mount/mnt: Mount remote storage" | |
| Write-Host " - unmount/umount/umnt: Unmount mount point" | |
| Write-Host " - status: Check mount status" | |
| Write-Host " - show: Show all mounted rclone" | |
| Write-Host " - help: Show this help" | |
| Write-Host " remote_name Host name from SSH config" | |
| Write-Host " remote_path Optional, remote path (only for mount command)" | |
| Write-Host " custom_mount_point Optional, custom local mount point (only for mount command)" | |
| Write-Host " --all, --a Batch operation on all hosts (mount/unmount supported)" | |
| Write-Host "" | |
| Write-Host "Single mount examples:" -ForegroundColor Yellow | |
| Write-Host " .\rclonemm_en.ps1 mount mom # Mount mom root to default location" | |
| Write-Host " .\rclonemm_en.ps1 mount mom:/WEB/logs # Mount mom specified directory to default location" | |
| Write-Host " .\rclonemm_en.ps1 mount mom D:\custom\path # Mount mom root to custom location" | |
| Write-Host " .\rclonemm_en.ps1 mount mom:/WEB D:\custom\path # Mount mom specified directory to custom location" | |
| Write-Host " .\rclonemm_en.ps1 unmount mom # Unmount mom (auto-find mount point)" | |
| Write-Host " .\rclonemm_en.ps1 status mom # Check mom status (auto-find mount point)" | |
| Write-Host "" | |
| Write-Host "Batch operation examples:" -ForegroundColor Yellow | |
| Write-Host " .\rclonemm_en.ps1 mount --all # Mount all SSH Hosts (auto-skip failures)" | |
| Write-Host " .\rclonemm_en.ps1 mount --a # Same as above (shorthand)" | |
| Write-Host " .\rclonemm_en.ps1 unmount --all # Unmount all rclone mounts" | |
| Write-Host " .\rclonemm_en.ps1 umount --a # Same as above (shorthand)" | |
| Write-Host "" | |
| Write-Host "Other examples:" -ForegroundColor Yellow | |
| Write-Host " .\rclonemm_en.ps1 show # Show all mounts" | |
| Write-Host " .\rclonemm_en.ps1 help # Show this help" | |
| Write-Host "" | |
| Write-Host "Available SSH Hosts:" -ForegroundColor Yellow | |
| Get-AvailableRemotes | |
| } | |
| <# | |
| .SYNOPSIS | |
| Check if SSH Host exists in config file | |
| #> | |
| function Test-RemoteExists { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| Write-ColorMessage -Level ERROR -Message "SSH config does not exist: $sshConfigPath" | |
| return $false | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $found = $content | Where-Object { $_ -match "^\s*Host\s+.*\b$HostAlias\b" } | |
| if (-not $found) { | |
| Write-ColorMessage -Level ERROR -Message "SSH Host '$HostAlias' does not exist in SSH config" | |
| Write-Host "Available Hosts: " -NoNewline | |
| Get-AvailableRemotes | |
| return $false | |
| } | |
| return $true | |
| } | |
| # ==================== Main Program ==================== | |
| function Main { | |
| # Check dependencies | |
| Test-Dependencies | |
| # Clean up old logs | |
| Clear-OldLogs | |
| # Check parameters | |
| if (-not $Operation) { | |
| Write-ColorMessage -Level ERROR -Message "Missing required parameters" | |
| Show-Help | |
| exit 1 | |
| } | |
| # Handle help operation | |
| if ($Operation -in @('help', '-h', '--help')) { | |
| Show-Help | |
| exit 0 | |
| } | |
| # Handle show operation (no remote name needed) | |
| if ($Operation -eq 'show') { | |
| Show-RcloneMounts | |
| exit 0 | |
| } | |
| # Handle batch operations (--all or -a) | |
| if ($RemoteSpec -in @('--all', '--a')) { | |
| # Ensure log directory exists | |
| $logDir = Split-Path $script:LogFile -Parent | |
| if (-not (Test-Path $logDir)) { | |
| New-Item -Path $logDir -ItemType Directory -Force | Out-Null | |
| } | |
| Write-ColorMessage -Level INFO -Message "Batch operation: $Operation all remotes, log: $($script:LogFile)" | |
| switch ($Operation) { | |
| { $_ -in @('mount', 'mnt') } { | |
| Mount-AllRemotes | |
| exit 0 | |
| } | |
| { $_ -in @('unmount', 'umount', 'umnt') } { | |
| Dismount-AllRemotes | |
| exit 0 | |
| } | |
| default { | |
| Write-ColorMessage -Level ERROR -Message "Operation '$Operation' does not support --all/--a parameter" | |
| Write-ColorMessage -Level INFO -Message "Operations supporting --all/--a: mount, unmount" | |
| exit 1 | |
| } | |
| } | |
| } | |
| # Check if remote spec is provided | |
| if (-not $RemoteSpec) { | |
| Write-ColorMessage -Level ERROR -Message "Missing SSH Host name parameter or --all/--a option" | |
| Show-Help | |
| exit 1 | |
| } | |
| # Parse host alias and path | |
| # Format: host_alias or host_alias:path | |
| $hostAlias = '' | |
| $remotePath = '' | |
| $pattern = '^(.+?):(.*)$' | |
| if ($RemoteSpec -match $pattern) { | |
| $hostAlias = $Matches[1] | |
| $remotePath = $Matches[2] | |
| } else { | |
| $hostAlias = $RemoteSpec | |
| $remotePath = '' | |
| } | |
| # Check if SSH Host exists | |
| if (-not (Test-RemoteExists -HostAlias $hostAlias)) { | |
| exit 1 | |
| } | |
| # Ensure log directory exists | |
| $logDir = Split-Path $script:LogFile -Parent | |
| if (-not (Test-Path $logDir)) { | |
| New-Item -Path $logDir -ItemType Directory -Force | Out-Null | |
| } | |
| # Execute corresponding operation | |
| Write-ColorMessage -Level INFO -Message "Operation: $Operation, log: $($script:LogFile)" | |
| switch ($Operation) { | |
| { $_ -in @('mount', 'mnt') } { | |
| Mount-RcloneRemote -HostAlias $hostAlias -RemotePath $remotePath -CustomMountPoint $CustomMountPoint | |
| } | |
| { $_ -in @('unmount', 'umount', 'umnt') } { | |
| Dismount-RcloneRemote -HostAlias $hostAlias -CustomMountPoint $CustomMountPoint | |
| } | |
| 'status' { | |
| Get-RcloneStatus -HostAlias $hostAlias -RemotePath $remotePath | |
| } | |
| default { | |
| Write-ColorMessage -Level ERROR -Message "Unknown operation: $Operation" | |
| Show-Help | |
| exit 1 | |
| } | |
| } | |
| } | |
| # Execute main program | |
| Main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # rclone mount management script | |
| # Features: mount, unmount, status, help | |
| # Usage: rclonemm [operation] <remote_name>[:<remote_path>] [custom_mount_point] | |
| # Example: rclonemm mount mom:/WEB/logs /custom/path | |
| # Author: GitHub Copilot | |
| # Date: 2025-11-04 | |
| # Update: 2025-11-12 - Added custom mount point support | |
| # Update: 2025-12-19 - Using SSH config, no longer depends on rclone.conf | |
| set -euo pipefail | |
| # Constants | |
| # Configuration file paths | |
| # Set mount base directory based on environment variable | |
| if [[ -n "${PREFIX:-}" ]]; then | |
| echo "Using custom PREFIX: $PREFIX" | |
| else | |
| PREFIX="" | |
| fi | |
| SSH_CONFIG="$HOME/.ssh/config" | |
| MOUNT_BASE_DIR="$PREFIX/mnt" | |
| LOG_DIR="$PREFIX/var/log" | |
| LOG_FILE="${LOG_DIR}/rclone_mount_$(date '+%Y%m%d').log" | |
| # Variables | |
| # Log retention days | |
| LOG_RETENTION_DAYS=7 | |
| # Mount timeout setting (seconds) | |
| MOUNT_TIMEOUT=10 | |
| ACCESS_TEST_TIMEOUT=2 | |
| # Get current user's UID and GID | |
| USER="${USER:-$(whoami)}" | |
| USER_UID=$(id -u "$USER") | |
| USER_GID=$(id -g "$USER") | |
| # Basic RCLONE parameters | |
| RCLONE_MOUNT_OPTIONS="--allow-other \ | |
| --vfs-cache-mode full \ | |
| --default-permissions \ | |
| --uid $USER_UID \ | |
| --gid $USER_GID \ | |
| --multi-thread-streams 4 \ | |
| --multi-thread-cutoff 100M \ | |
| --low-level-retries 2 \ | |
| --log-level ERROR \ | |
| --log-file $LOG_FILE" | |
| # Basic SSH command | |
| ssh_cmd="ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%r@%h:%p -o ControlPersist=10 -o ConnectTimeout=2" | |
| # Dependency check function | |
| check_dependencies() { | |
| local missing_deps=() | |
| # Check rclone | |
| if ! command -v rclone >/dev/null 2>&1; then | |
| missing_deps+=("rclone") | |
| fi | |
| # Check fusermount or fusermount3 | |
| if ! command -v fusermount >/dev/null 2>&1 && ! command -v fusermount3 >/dev/null 2>&1; then | |
| missing_deps+=("fusermount (fuse)") | |
| fi | |
| # Check sudo | |
| if ! command -v sudo >/dev/null 2>&1; then | |
| missing_deps+=("sudo") | |
| fi | |
| # If there are missing dependencies, show error and exit | |
| if [[ ${#missing_deps[@]} -gt 0 ]]; then | |
| echo -e "${RED}[ERROR]${NC} Missing required dependencies:" | |
| for dep in "${missing_deps[@]}"; do | |
| echo -e " ${RED}✗${NC} $dep" | |
| done | |
| echo | |
| echo -e "${YELLOW}Please install the missing packages:${NC}" | |
| echo " Debian/Ubuntu: sudo apt-get install rclone fuse3 sudo" | |
| echo " Alpine: apk add rclone fuse3 sudo" | |
| echo " Termux: pkg install rclone libfuse3 tsu" | |
| exit 1 | |
| fi | |
| } | |
| # Check if ps command is available | |
| has_ps_command() { | |
| command -v ps >/dev/null 2>&1 | |
| } | |
| # Safely execute ps command | |
| safe_ps() { | |
| if has_ps_command; then | |
| safe_ps 2>/dev/null || ps -ef 2>/dev/null | |
| else | |
| # No ps command, return empty | |
| return 1 | |
| fi | |
| } | |
| # === SSH Config parsing functions === | |
| # Parse SSH config to get specified property for a Host | |
| parse_ssh_config() { | |
| local host_alias="$1" | |
| local key="$2" | |
| awk -v host="$host_alias" -v key="$key" ' | |
| tolower($1) == "host" { | |
| for (i = 2; i <= NF; i++) { | |
| if ($i == host) { | |
| found = 1 | |
| next | |
| } | |
| } | |
| found = 0 | |
| } | |
| found && tolower($1) == tolower(key) { | |
| print $2 | |
| exit | |
| } | |
| ' "$SSH_CONFIG" | |
| } | |
| # Get complete SSH Host configuration | |
| get_ssh_host_config() { | |
| local host_alias="$1" | |
| local hostname user port identity_file proxy_jump | |
| hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| user=$(parse_ssh_config "$host_alias" "User") | |
| port=$(parse_ssh_config "$host_alias" "Port") | |
| identity_file=$(parse_ssh_config "$host_alias" "IdentityFile") | |
| proxy_jump=$(parse_ssh_config "$host_alias" "ProxyJump") | |
| # If no explicit HostName, use Host alias | |
| hostname="${hostname:-$host_alias}" | |
| # Expand ~ to actual path | |
| if [[ -n "$identity_file" ]]; then | |
| identity_file="${identity_file/#\~/$HOME}" | |
| fi | |
| # Output format: hostname|user|port|identity_file|proxy_jump | |
| echo "$hostname|$user|$port|$identity_file|$proxy_jump" | |
| } | |
| # Build rclone SFTP backend string (using key_file) | |
| # Parameters: $1 = host_alias or config_data | |
| build_sftp_backend_with_key() { | |
| local input="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| # Determine if input is host_alias or config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # Already config_data | |
| config_data="$input" | |
| else | |
| # Is host_alias, need to get config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # Basic backend string | |
| local backend=":sftp,host=$hostname" | |
| # Add optional parameters | |
| [[ -n "$user" ]] && backend="$backend,user=$user" | |
| [[ -n "$port" ]] && backend="$backend,port=$port" | |
| [[ -n "$key_file" ]] && backend="$backend,key_file=$key_file" | |
| echo "$backend" | |
| } | |
| # Build rclone SFTP backend string (without key_file, for use with --sftp-ssh) | |
| # Parameters: $1 = host_alias or config_data | |
| build_sftp_backend_without_key() { | |
| local input="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| # Determine if input is host_alias or config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # Already config_data | |
| config_data="$input" | |
| else | |
| # Is host_alias, need to get config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # Basic backend string | |
| local backend=":sftp,host=$hostname" | |
| # Add optional parameters | |
| [[ -n "$user" ]] && backend="$backend,user=$user" | |
| [[ -n "$port" ]] && backend="$backend,port=$port" | |
| echo "$backend" | |
| } | |
| # Check if key file exists | |
| # Parameters: $1 = host_alias or config_data | |
| has_key_file() { | |
| local input="$1" | |
| local config_data key_file | |
| # Determine if input is host_alias or config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # Already config_data | |
| config_data="$input" | |
| else | |
| # Is host_alias, need to get config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r _ _ _ key_file _ <<< "$config_data" | |
| [[ -n "$key_file" ]] && [[ -f "$key_file" ]] | |
| } | |
| # Build --sftp-ssh option | |
| # Parameters: $1 = host_alias or config_data | |
| build_sftp_ssh_option() { | |
| local input="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| # Determine if input is host_alias or config_data | |
| if [[ "$input" == *"|"* ]]; then | |
| # Already config_data | |
| config_data="$input" | |
| else | |
| # Is host_alias, need to get config | |
| config_data=$(get_ssh_host_config "$input") | |
| fi | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # Add Port | |
| if [[ -n "$port" ]]; then | |
| ssh_cmd="$ssh_cmd -p $port" | |
| fi | |
| # Add IdentityFile | |
| if [[ -n "$key_file" ]]; then | |
| ssh_cmd="$ssh_cmd -i $key_file" | |
| fi | |
| # Add ProxyJump | |
| if [[ -n "$proxy_jump" ]]; then | |
| ssh_cmd="$ssh_cmd -J $proxy_jump" | |
| fi | |
| # Build complete SSH connection string user@hostname | |
| local ssh_target="" | |
| if [[ -n "$user" ]]; then | |
| ssh_target="${user}@${hostname}" | |
| else | |
| ssh_target="${hostname}" | |
| fi | |
| echo "--sftp-ssh \"$ssh_cmd $ssh_target\"" | |
| } | |
| # === Mount point lookup functions === | |
| # Find all mount points for specified host_alias via mount command | |
| # Return format: each line contains "remote_path|mount_point" | |
| find_mount_points_by_mount() { | |
| local host_alias="$1" | |
| # Get hostname | |
| # local hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| # hostname="${hostname:-$host_alias}" | |
| # Find all matching mounts from mount output | |
| # Support formats: hostname: or hostname{xxx}: | |
| mount | grep "type fuse.rclone" 2>/dev/null | while read -r line; do | |
| [[ -z "$line" ]] && continue | |
| local remote_part | |
| remote_part=$(echo "$line" | awk '{print $1}') | |
| local mount_point | |
| mount_point=$(echo "$line" | awk '{print $3}') | |
| # Check if remote_part starts with hostname | |
| # Support hostname: or hostname{xxx}: format | |
| if [[ "$remote_part" =~ ^${host_alias}(\{[^}]+\})?:(.*)$ ]]; then | |
| # Extract remote_path (part after colon) | |
| local remote_path="${BASH_REMATCH[2]}" | |
| # Output format: remote_path|mount_point | |
| echo "${remote_path}|${mount_point}" | |
| fi | |
| done || true | |
| } | |
| # Find rclone process PIDs matching host_alias and mount point | |
| # Parameters: $1 = host_alias, $2 = mount_point (optional) | |
| find_rclone_pids() { | |
| local host_alias="$1" | |
| local mount_point="${2:-}" | |
| if [[ -n "$mount_point" ]]; then | |
| # Match both host_alias and mount point | |
| safe_ps | grep "rclone.*$host_alias" | grep "$mount_point" | grep -v grep | grep -v "rclonemm" | awk '{print $1}' | grep -E '^[0-9]+$' | |
| else | |
| # Match only host_alias | |
| safe_ps | grep "rclone.*$host_alias" | grep -v grep | grep -v "rclonemm" | awk '{print $1}' | grep -E '^[0-9]+$' | |
| fi | |
| } | |
| # Color definitions | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| # Cleanup old logs | |
| cleanup_old_logs() { | |
| # Ensure log directory exists | |
| if [[ ! -d "$LOG_DIR" ]]; then | |
| return 0 | |
| fi | |
| # Delete log files older than retention days | |
| # Format: rclone_mount_YYYYMMDD.log | |
| sudo find "$LOG_DIR" -name "rclone_mount_*.log" -type f -mtime +${LOG_RETENTION_DAYS} -delete 2>/dev/null || true | |
| } | |
| # Show usage help | |
| show_help() { | |
| echo -e "${BLUE}rclone Mount Management Tool${NC}" | |
| echo | |
| echo -e "${YELLOW}Usage:${NC}" | |
| echo " $(basename "$0") <operation> <remote_name>[:<remote_path>] [custom_mount_point]" | |
| echo " $(basename "$0") <operation> --all|-a" | |
| echo | |
| echo -e "${YELLOW}Parameters:${NC}" | |
| echo " operation Operation type" | |
| echo " - mount/mnt: Mount remote storage" | |
| echo " - unmount/umount/umnt: Unmount mount point" | |
| echo " - status: Check mount status" | |
| echo " - show: Show all mounted rclone" | |
| echo " - help: Show this help" | |
| echo " remote_name Host name from SSH config" | |
| echo " remote_path Optional, remote path (only for mount command)" | |
| echo " custom_mount_point Optional, custom local mount point (only for mount command)" | |
| echo " --all, -a Batch operation on all hosts (mount/unmount supported)" | |
| echo | |
| echo -e "${YELLOW}Single mount examples:${NC}" | |
| echo " $(basename "$0") mount mom # Mount mom root to default location" | |
| echo " $(basename "$0") mount mom:/WEB/logs # Mount mom specified directory to default location" | |
| echo " $(basename "$0") mount mom /custom/path # Mount mom root to custom location" | |
| echo " $(basename "$0") mount mom:/WEB /custom/path # Mount mom specified directory to custom location" | |
| echo " $(basename "$0") unmount mom # Unmount mom (auto-find mount point)" | |
| echo " $(basename "$0") status mom # Check mom status (auto-find mount point)" | |
| echo | |
| echo -e "${YELLOW}Batch operation examples:${NC}" | |
| echo " $(basename "$0") mount --all # Mount all SSH Hosts (auto-skip failures)" | |
| echo " $(basename "$0") mount -a # Same as above (shorthand)" | |
| echo " $(basename "$0") unmount --all # Unmount all rclone mounts" | |
| echo " $(basename "$0") umount -a # Same as above (shorthand)" | |
| echo | |
| echo -e "${YELLOW}Other examples:${NC}" | |
| echo " $(basename "$0") show # Show all mounts" | |
| echo " $(basename "$0") help # Show this help" | |
| echo | |
| echo -e "${YELLOW}Available SSH Hosts:${NC}" | |
| get_available_remotes | |
| } | |
| # Log message | |
| log_message() { | |
| local level="$1" | |
| local message="$2" | |
| local timestamp | |
| timestamp=$(date '+%Y-%m-%d %H:%M:%S') | |
| echo "[$timestamp] [$level] $message" | sudo tee -a "$LOG_FILE" >/dev/null 2>&1 || true | |
| } | |
| # Print message | |
| print_message() { | |
| local level="$1" | |
| local message="$2" | |
| local color="" | |
| case "$level" in | |
| "INFO") color="$GREEN" ;; | |
| "WARN") color="$YELLOW" ;; | |
| "ERROR") color="$RED" ;; | |
| "DEBUG") color="$BLUE" ;; | |
| esac | |
| echo -e "${color}[$level]${NC} $message" | |
| # Avoid log_message failure causing script exit | |
| log_message "$level" "$message" || true | |
| } | |
| # Get available SSH Host list | |
| get_available_remotes() { | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| return 1 | |
| fi | |
| # Get all Host names (exclude wildcards and special purpose) | |
| local hosts | |
| hosts=$(grep "^Host " "$SSH_CONFIG" | awk '{print $2}' | grep -v '\*' | grep -v 'github.com' | sort) | |
| # Process each host line by line | |
| while IFS= read -r host; do | |
| [[ -z "$host" ]] && continue | |
| # Check if this host is already mounted | |
| local mount_info | |
| mount_info=$(find_mount_points_by_mount "$host") | |
| if [[ -n "$mount_info" ]]; then | |
| # Count number of mounts | |
| local mount_count | |
| mount_count=$(echo "$mount_info" | wc -l) | |
| # Get first mount point | |
| local first_mount | |
| first_mount=$(echo "$mount_info" | head -1 | cut -d'|' -f2) | |
| if [[ $mount_count -gt 1 ]]; then | |
| echo -e "$host ${YELLOW}[mounted ${mount_count}: $first_mount ...]${NC}" | |
| else | |
| echo -e "$host ${YELLOW}[mounted: $first_mount]${NC}" | |
| fi | |
| else | |
| echo "$host" | |
| fi | |
| done <<< "$hosts" | |
| } | |
| # Check if SSH Host exists in config file | |
| check_remote_exists() { | |
| local host_alias="$1" | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| print_message "ERROR" "SSH config does not exist: $SSH_CONFIG" | |
| return 1 | |
| fi | |
| if ! grep -q "^Host .*\\b$host_alias\\b" "$SSH_CONFIG"; then | |
| print_message "ERROR" "SSH Host '$host_alias' does not exist in SSH config" | |
| print_message "INFO" "Available Hosts: $(get_available_remotes | tr '\n' ' ')" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # Check and create mount point | |
| ensure_mount_point() { | |
| local mount_point="$1" | |
| if [[ ! -d "$mount_point" ]]; then | |
| print_message "INFO" "Creating mount point: $mount_point" | |
| sudo mkdir -p "$mount_point" | |
| sudo chown "$USER:$USER" "$mount_point" | |
| fi | |
| # Check directory permissions | |
| if [[ ! -w "$mount_point" ]]; then | |
| print_message "WARN" "Mount point has no write permission, attempting to fix" | |
| sudo chown "$USER:$USER" "$mount_point" | |
| fi | |
| } | |
| # Check if already mounted | |
| is_mounted() { | |
| local mount_point="$1" | |
| mount | grep -q " $mount_point " || return 1 | |
| return 0 | |
| } | |
| # Core mount function (single host mount logic, includes timeout waiting) | |
| # Parameters: $1 = host_alias, $2 = remote_path (optional), $3 = mount_point | |
| # Returns: 0 success, 1 failure | |
| do_mount_single() { | |
| local host_alias="$1" | |
| local remote_path="${2:-}" | |
| local mount_point="$3" | |
| local sftp_ssh_option="" | |
| local remote_source | |
| local config_data | |
| local hostname user port key_file proxy_jump | |
| # Get SSH Host configuration | |
| config_data=$(get_ssh_host_config "$host_alias") | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # Uniformly use --sftp-ssh option for mounting | |
| sftp_ssh_option=$(build_sftp_ssh_option "$config_data") | |
| # Create rclone config before mounting (use pass parameter to avoid interactive prompts) | |
| if ! sudo rclone config create "$host_alias" sftp pass "" >/dev/null 2>&1; then | |
| print_message "ERROR" "Failed to create rclone config" | |
| return 1 | |
| fi | |
| # Combine remote source path | |
| if [[ -n "$remote_path" ]]; then | |
| remote_source="${host_alias}:$remote_path" | |
| else | |
| remote_source="${host_alias}:" | |
| fi | |
| # Check if target mount point is occupied | |
| if is_mounted "$mount_point"; then | |
| print_message "ERROR" "Mount point $mount_point is already occupied by another filesystem" | |
| local existing_mount | |
| existing_mount=$(mount | grep " $mount_point ") | |
| print_message "INFO" "Existing mount: $existing_mount" | |
| # Clear rclone config | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 1 | |
| fi | |
| # Ensure mount point exists | |
| ensure_mount_point "$mount_point" | |
| # Build rclone command | |
| local cmd="sudo rclone mount $remote_source $mount_point" | |
| cmd="$cmd $RCLONE_MOUNT_OPTIONS" | |
| cmd="$cmd $sftp_ssh_option" | |
| cmd="$cmd --daemon" | |
| print_message "INFO" "Mount command: $cmd" | |
| print_message "INFO" "Tip: If SSH requires password, enter it when prompted" | |
| # Execute mount command (error handling) | |
| # Use variable to capture exit code, avoid set -e causing script exit | |
| # Note: Do not restore set -e, let caller control error handling behavior | |
| local saved_errexit | |
| if [[ $- =~ e ]]; then | |
| saved_errexit=1 | |
| else | |
| saved_errexit=0 | |
| fi | |
| set +e | |
| eval "$cmd" 2>&1 | |
| local mount_exit_code=$? | |
| if [[ $saved_errexit -eq 1 ]]; then | |
| set -e | |
| fi | |
| if [[ $mount_exit_code -ne 0 ]]; then | |
| print_message "ERROR" "rclone mount command failed (exit code: $mount_exit_code), see log for details: $LOG_FILE" | |
| print_message "WARN" "Performing cleanup..." | |
| # Clear rclone config | |
| print_message "INFO" "Clearing rclone config..." | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| # Delete mount point (if default location) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]] && [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "Deleting mount point: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" 2>/dev/null || true | |
| fi | |
| return 1 | |
| fi | |
| # Wait for mount to complete | |
| local count=0 | |
| while [[ $count -lt $MOUNT_TIMEOUT ]]; do | |
| print_message "INFO" "Waiting for mount... ($((count + 1))/$MOUNT_TIMEOUT)" | |
| sleep 1 || true | |
| if is_mounted "$mount_point"; then | |
| print_message "INFO" "Mount successful: $host_alias:$remote_path -> $mount_point" | |
| # Test if mount point is accessible | |
| print_message "INFO" "Testing mount point access..." | |
| if timeout $ACCESS_TEST_TIMEOUT ls "$mount_point" >/dev/null 2>&1; then | |
| print_message "INFO" "Mount point test passed, log: $LOG_FILE" | |
| # Clear rclone config after successful mount | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 0 | |
| else | |
| print_message "ERROR" "Mount point inaccessible, mount failed" | |
| print_message "WARN" "Performing cleanup..." | |
| # Perform cleanup (don't call unmount_remote to avoid circular dependency) | |
| sudo umount "$mount_point" 2>/dev/null || sudo umount -f -l "$mount_point" 2>/dev/null || true | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| echo "$rclone_pids" | xargs sudo kill -TERM 2>/dev/null || true | |
| sleep 1 || true | |
| fi | |
| # Clear rclone config | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 1 | |
| fi | |
| fi | |
| count=$((count + 1)) | |
| done | |
| print_message "ERROR" "Mount timeout, please check log: $LOG_FILE" | |
| print_message "WARN" "Performing cleanup..." | |
| # Perform cleanup | |
| sudo umount "$mount_point" 2>/dev/null || sudo umount -f -l "$mount_point" 2>/dev/null || true | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| echo "$rclone_pids" | xargs sudo kill -TERM 2>/dev/null || true | |
| sleep 1 || true | |
| fi | |
| # Clear rclone config | |
| sudo rclone config delete "$host_alias" >/dev/null 2>&1 || true | |
| return 1 | |
| } | |
| # Mount remote (user interface function) | |
| mount_remote() { | |
| local host_alias="$1" | |
| local remote_path="${2:-}" | |
| local custom_mount_point="${3:-}" | |
| local mount_point | |
| # Determine mount point: custom > default | |
| if [[ -n "$custom_mount_point" ]]; then | |
| mount_point="$custom_mount_point" | |
| else | |
| mount_point="$MOUNT_BASE_DIR/$host_alias" | |
| fi | |
| print_message "INFO" "Using --sftp-ssh option for mounting" | |
| print_message "INFO" "Mounting $host_alias:$remote_path to $mount_point" | |
| if [[ -n "$custom_mount_point" ]]; then | |
| print_message "INFO" "Using custom mount point: $custom_mount_point" | |
| fi | |
| # Call core mount function | |
| do_mount_single "$host_alias" "$remote_path" "$mount_point" | |
| return $? | |
| } | |
| # Unmount remote | |
| unmount_remote() { | |
| local host_alias="$1" | |
| local custom_mount_point="${2:-}" | |
| # local hostname | |
| # # Get hostname for process identification | |
| # hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| # hostname="${hostname:-$host_alias}" | |
| # Find all mount points | |
| local mount_info | |
| mount_info=$(find_mount_points_by_mount "$host_alias") | |
| if [[ -z "$mount_info" ]]; then | |
| print_message "WARN" "$host_alias is not mounted" | |
| # Check if there are still related rclone processes running | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias") | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "Found residual rclone processes: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "Terminating processes..." | |
| echo "$rclone_pids" | xargs sudo kill | |
| sleep 2 | |
| print_message "INFO" "rclone processes terminated" | |
| fi | |
| return 0 | |
| fi | |
| # If remote_path is specified, check if it exists | |
| if [[ -n "$custom_mount_point" ]]; then | |
| local found=false | |
| local remote_path | |
| local target_mount_point="" | |
| while IFS='|' read -r existing_path existing_point; do | |
| if [[ "$existing_point" == "$custom_mount_point" ]]; then | |
| found=true | |
| target_mount_point="$existing_point" | |
| remote_path="$existing_path" | |
| break | |
| fi | |
| done <<< "$mount_info" | |
| if [[ "$found" == false ]]; then | |
| print_message "ERROR" "Cannot find mounted path: $host_alias $custom_mount_point" | |
| print_message "INFO" "Current $host_alias mounts:" | |
| while IFS='|' read -r existing_path existing_point; do | |
| local path_display="${existing_path:-~}" | |
| print_message "INFO" " [$path_display] -> $existing_point" | |
| done <<< "$mount_info" | |
| return 1 | |
| fi | |
| # Perform single unmount | |
| print_message "INFO" "Unmounting $host_alias:$remote_path on $custom_mount_point" | |
| perform_unmount "$target_mount_point" "$host_alias" | |
| return $? | |
| else | |
| # No remote_path specified, ask for confirmation one by one | |
| print_message "INFO" "Found $(echo "$mount_info" | wc -l) mount(s) for $host_alias:" | |
| # Use process substitution to avoid subshell, let read get input from /dev/tty | |
| while IFS='|' read -r existing_path existing_point; do | |
| local path_display="${existing_path:-~}" | |
| print_message "INFO" "" | |
| print_message "INFO" "Mount: [$path_display] on $existing_point" | |
| echo -n "Do you want to unmount this? [y/N]: " | |
| # Read from /dev/tty to ensure user input | |
| read -r answer </dev/tty || { | |
| echo | |
| print_message "WARN" "Input interrupted, canceling operation" | |
| return 1 | |
| } | |
| if [[ "$answer" =~ ^[Yy]$ ]]; then | |
| perform_unmount "$existing_point" "$host_alias" | |
| else | |
| print_message "INFO" "Skipping $existing_point" | |
| fi | |
| done <<< "$mount_info" | |
| return 0 | |
| fi | |
| } | |
| # Perform actual unmount operation | |
| perform_unmount() { | |
| local mount_point="$1" | |
| local host_alias="$2" | |
| # local hostname="$3" | |
| print_message "INFO" "Unmounting $mount_point" | |
| # Try normal unmount | |
| if sudo umount "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "Unmount successful: $mount_point" | |
| # Ensure related processes are terminated | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "Found related rclone processes: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "Terminating processes..." | |
| echo "$rclone_pids" | xargs sudo kill 2>/dev/null || true | |
| sleep 1 || true | |
| print_message "INFO" "rclone processes terminated" | |
| fi | |
| # Delete default mount point, try to delete custom mount point (if empty) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]]; then | |
| if [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "Deleting mount point: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" | |
| fi | |
| else | |
| # Custom mount point: try to delete empty directory, keep if not empty | |
| if [[ -d "$mount_point" ]]; then | |
| if sudo rmdir "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "Deleted empty custom mount point: $mount_point" | |
| else | |
| print_message "INFO" "Keeping non-empty custom mount point: $mount_point" | |
| fi | |
| fi | |
| fi | |
| return 0 | |
| fi | |
| # Force unmount | |
| print_message "WARN" "Normal unmount failed, trying force unmount" | |
| if sudo umount -f -l "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "Force unmount successful: $mount_point" | |
| # Ensure related processes are terminated | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "Found related rclone processes: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "Terminating processes..." | |
| echo "$rclone_pids" | xargs sudo kill 2>/dev/null || true | |
| sleep 1 || true | |
| print_message "INFO" "rclone processes terminated" | |
| fi | |
| # Delete default mount point, try to delete custom mount point (if empty) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]]; then | |
| if [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "Deleting mount point: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" | |
| fi | |
| else | |
| # Custom mount point: try to delete empty directory, keep if not empty | |
| if [[ -d "$mount_point" ]]; then | |
| if sudo rmdir "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "Deleted empty custom mount point: $mount_point" | |
| else | |
| print_message "INFO" "Keeping non-empty custom mount point: $mount_point" | |
| fi | |
| fi | |
| fi | |
| return 0 | |
| fi | |
| # Last resort: kill rclone process first then try unmount | |
| print_message "WARN" "Force unmount failed, trying to terminate rclone process first" | |
| local rclone_pids | |
| rclone_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$rclone_pids" ]]; then | |
| print_message "INFO" "Found rclone processes: $(echo "$rclone_pids" | tr '\n' ' ')" | |
| print_message "INFO" "Sending TERM signal..." | |
| echo "$rclone_pids" | xargs sudo kill -TERM 2>/dev/null || true | |
| sleep 3 || true | |
| # Check if processes are still running, force kill if so | |
| local remaining_pids | |
| remaining_pids=$(find_rclone_pids "$host_alias" "$mount_point" || true) | |
| if [[ -n "$remaining_pids" ]]; then | |
| print_message "WARN" "Processes still running: $(echo "$remaining_pids" | tr '\n' ' ')" | |
| print_message "INFO" "Sending KILL signal to force terminate..." | |
| echo "$remaining_pids" | xargs sudo kill -KILL 2>/dev/null || true | |
| sleep 2 || true | |
| print_message "INFO" "rclone processes force terminated" | |
| else | |
| print_message "INFO" "rclone processes terminated" | |
| fi | |
| # Try unmount again | |
| sudo umount "$mount_point" 2>/dev/null || true | |
| print_message "INFO" "Unmount after process termination successful: $mount_point" | |
| # Delete default mount point, try to delete custom mount point (if empty) | |
| if [[ "$mount_point" == "$MOUNT_BASE_DIR/$host_alias" ]]; then | |
| if [[ -d "$mount_point" ]]; then | |
| print_message "INFO" "Deleting mount point: $mount_point" | |
| sudo rmdir "$mount_point" 2>/dev/null || sudo rm -rf "$mount_point" | |
| fi | |
| else | |
| # Custom mount point: try to delete empty directory, keep if not empty | |
| if [[ -d "$mount_point" ]]; then | |
| if sudo rmdir "$mount_point" 2>/dev/null; then | |
| print_message "INFO" "Deleted empty custom mount point: $mount_point" | |
| else | |
| print_message "INFO" "Keeping non-empty custom mount point: $mount_point" | |
| fi | |
| fi | |
| fi | |
| return 0 | |
| fi | |
| print_message "ERROR" "Unmount failed: $mount_point" | |
| return 1 | |
| } | |
| # Check mount status | |
| check_status() { | |
| local host_alias="$1" | |
| local remote_path="${2:-}" | |
| print_message "INFO" "Checking mount status for $host_alias" | |
| # Find all mount points | |
| local mount_info | |
| mount_info=$(find_mount_points_by_mount "$host_alias") | |
| if [[ -z "$mount_info" ]]; then | |
| print_message "INFO" "Not mounted" | |
| return 0 | |
| fi | |
| # If remote_path is specified, only show matching ones | |
| if [[ -n "$remote_path" ]]; then | |
| local found=false | |
| while IFS='|' read -r existing_path mount_point; do | |
| if [[ "$existing_path" == "$remote_path" ]]; then | |
| found=true | |
| local full_mount_info | |
| full_mount_info=$(mount | grep " $mount_point ") | |
| print_message "INFO" "Mounted: $full_mount_info" | |
| # Check if mount point is accessible | |
| if timeout 5 ls "$mount_point" >/dev/null 2>&1; then | |
| print_message "INFO" "Mount point accessible" | |
| else | |
| print_message "WARN" "Mount point inaccessible, may need remounting" | |
| fi | |
| fi | |
| done <<< "$mount_info" | |
| if [[ "$found" == false ]]; then | |
| print_message "INFO" "Specified path $remote_path is not mounted" | |
| fi | |
| else | |
| # Show all mounts | |
| print_message "INFO" "Found $(echo "$mount_info" | wc -l) mount(s):" | |
| while IFS='|' read -r existing_path mount_point; do | |
| local path_display="${existing_path:-~}" | |
| print_message "INFO" " [$path_display] -> $mount_point" | |
| # Check if mount point is accessible | |
| if timeout 2 ls "$mount_point" >/dev/null 2>&1; then | |
| print_message "INFO" " Accessible" | |
| else | |
| print_message "WARN" " Inaccessible" | |
| fi | |
| done <<< "$mount_info" | |
| fi | |
| } | |
| # Show all mounted rclone | |
| show_all_mounts() { | |
| print_message "INFO" "Checking all rclone mounts..." | |
| echo | |
| # Get all rclone type mounts | |
| local mounts | |
| mounts=$(mount | grep "type fuse.rclone") | |
| if [[ -z "$mounts" ]]; then | |
| print_message "INFO" "No rclone mounts currently" | |
| return 0 | |
| fi | |
| echo -e "${GREEN}=== Mounted rclone remotes ===${NC}" | |
| echo | |
| # Parse and display each mount | |
| while IFS= read -r line; do | |
| # Extract remote name and mount point | |
| local remote | |
| remote=$(echo "$line" | awk '{print $1}' | cut -d: -f1) | |
| local mount_point | |
| mount_point=$(echo "$line" | awk '{print $3}') | |
| echo -e "${YELLOW}Remote:${NC} $remote" | |
| echo -e "${YELLOW}Mount point:${NC} $mount_point" | |
| # Test mount point access | |
| if timeout 2 ls "$mount_point" >/dev/null 2>&1; then | |
| echo -e "${YELLOW}Status:${NC} ${GREEN}Accessible${NC}" | |
| else | |
| echo -e "${YELLOW}Status:${NC} ${RED}Inaccessible${NC}" | |
| fi | |
| # Show mount details | |
| echo -e "${YELLOW}Details:${NC} $line" | |
| echo | |
| done <<< "$mounts" | |
| # Show related rclone processes | |
| local rclone_pids | |
| rclone_pids=$(safe_ps | grep "rclone mount" | grep -v grep | grep -v "rclonemm") | |
| if [[ -n "$rclone_pids" ]]; then | |
| echo -e "${GREEN}=== rclone processes ===${NC}" | |
| echo "$rclone_pids" | |
| fi | |
| } | |
| # Batch mount all available SSH hosts | |
| mount_all_remotes() { | |
| local success_count=0 | |
| local fail_count=0 | |
| local failed_hosts=() | |
| print_message "INFO" "Getting all available SSH Hosts..." | |
| # Get all available hosts | |
| local all_hosts | |
| all_hosts=$(grep "^Host " "$SSH_CONFIG" | awk '{print $2}' | grep -v '\*' | grep -v 'github.com' | sort) | |
| if [[ -z "$all_hosts" ]]; then | |
| print_message "WARN" "No SSH Hosts available for mounting" | |
| return 0 | |
| fi | |
| local total_hosts | |
| total_hosts=$(echo "$all_hosts" | wc -l) | |
| print_message "INFO" "Found $total_hosts SSH Hosts, starting batch mount..." | |
| echo | |
| # Temporarily disable errexit to avoid single failure interrupting entire flow | |
| set +e | |
| local current=0 | |
| while IFS= read -r host; do | |
| [[ -z "$host" ]] && continue | |
| ((current++)) | |
| local mount_point="$MOUNT_BASE_DIR/$host" | |
| # Check if already mounted | |
| if is_mounted "$mount_point"; then | |
| print_message "INFO" "[$current/$total_hosts] $host already mounted at $mount_point, skipping" | |
| ((success_count++)) | |
| continue | |
| fi | |
| print_message "INFO" "==== [$current/$total_hosts] Mounting $host ====" | |
| # Use core mount function | |
| if do_mount_single "$host" "" "$mount_point"; then | |
| ((success_count++)) | |
| print_message "INFO" "$host mount successful" | |
| else | |
| ((fail_count++)) | |
| failed_hosts+=("$host") | |
| print_message "WARN" "$host mount failed, continuing to next" | |
| fi | |
| echo | |
| done <<< "$all_hosts" | |
| # Restore errexit | |
| set -e | |
| # Show statistics | |
| echo | |
| print_message "INFO" "==== Batch mount completed ====" | |
| print_message "INFO" "Success: $success_count, Failed: $fail_count, Total: $total_hosts" | |
| if [[ $fail_count -gt 0 ]]; then | |
| print_message "WARN" "Failed hosts: ${failed_hosts[*]}" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # Batch unmount all rclone mounts | |
| unmount_all_remotes() { | |
| local success_count=0 | |
| local fail_count=0 | |
| local failed_mounts=() | |
| print_message "INFO" "Checking all rclone mounts..." | |
| # Get all rclone mounts | |
| local all_mounts | |
| all_mounts=$(mount | grep "type fuse.rclone" || true) | |
| if [[ -z "$all_mounts" ]]; then | |
| print_message "INFO" "No rclone mounts currently" | |
| return 0 | |
| fi | |
| local total_mounts | |
| total_mounts=$(echo "$all_mounts" | wc -l) | |
| print_message "INFO" "Found $total_mounts rclone mounts, starting batch unmount..." | |
| echo | |
| # Temporarily disable errexit | |
| set +e | |
| local current=0 | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| ((current++)) | |
| local mount_point | |
| mount_point=$(echo "$line" | awk '{print $3}') | |
| local remote_part | |
| remote_part=$(echo "$line" | awk '{print $1}') | |
| # Extract host alias (remove possible parameters and path) | |
| local host_alias | |
| if [[ "$remote_part" =~ ^([^{:]+) ]]; then | |
| host_alias="${BASH_REMATCH[1]}" | |
| else | |
| host_alias=$(echo "$remote_part" | cut -d: -f1) | |
| fi | |
| print_message "INFO" "==== [$current/$total_mounts] Unmounting $mount_point ====" | |
| # Use core unmount function perform_unmount | |
| if perform_unmount "$mount_point" "$host_alias"; then | |
| ((success_count++)) | |
| print_message "INFO" "$mount_point unmount successful" | |
| else | |
| ((fail_count++)) | |
| failed_mounts+=("$mount_point") | |
| print_message "WARN" "$mount_point unmount failed, continuing to next" | |
| fi | |
| echo | |
| done <<< "$all_mounts" | |
| # Restore errexit | |
| set -e | |
| # Show statistics | |
| echo | |
| print_message "INFO" "==== Batch unmount completed ====" | |
| print_message "INFO" "Success: $success_count, Failed: $fail_count, Total: $total_mounts" | |
| if [[ $fail_count -gt 0 ]]; then | |
| print_message "WARN" "Failed mount points: ${failed_mounts[*]}" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # Main function | |
| main() { | |
| # Check dependencies | |
| check_dependencies | |
| # Cleanup old logs | |
| cleanup_old_logs | |
| # Check parameters | |
| if [[ $# -eq 0 ]]; then | |
| print_message "ERROR" "Missing required parameters" | |
| show_help | |
| exit 1 | |
| fi | |
| local operation="$1" | |
| local remote_spec="${2:-}" | |
| local custom_mount_point="${3:-}" | |
| # Handle help operation | |
| if [[ "$operation" == "help" || "$operation" == "-h" || "$operation" == "--help" ]]; then | |
| show_help | |
| exit 0 | |
| fi | |
| # Handle show operation (no remote name needed) | |
| if [[ "$operation" == "show" ]]; then | |
| show_all_mounts | |
| exit 0 | |
| fi | |
| # Handle batch operations (--all or -a) | |
| if [[ "$remote_spec" == "--all" || "$remote_spec" == "-a" ]]; then | |
| # Ensure log directory exists | |
| sudo mkdir -p "$LOG_DIR" | |
| print_message "INFO" "Batch operation: $operation all remotes, log: $LOG_FILE" | |
| case "$operation" in | |
| "mount"|"mnt") | |
| mount_all_remotes | |
| exit $? | |
| ;; | |
| "unmount"|"umount"|"umnt") | |
| unmount_all_remotes | |
| exit $? | |
| ;; | |
| *) | |
| print_message "ERROR" "Operation '$operation' does not support --all/-a parameter" | |
| print_message "INFO" "Operations supporting --all/-a: mount, unmount" | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| # Check if remote spec is provided | |
| if [[ -z "$remote_spec" ]]; then | |
| print_message "ERROR" "Missing SSH Host name parameter or --all/-a option" | |
| show_help | |
| exit 1 | |
| fi | |
| # Parse host alias and path | |
| # Format: host_alias or host_alias:path | |
| local host_alias remote_path | |
| if [[ "$remote_spec" =~ ^([^:]+):(.*)$ ]]; then | |
| host_alias="${BASH_REMATCH[1]}" | |
| remote_path="${BASH_REMATCH[2]}" | |
| else | |
| host_alias="$remote_spec" | |
| remote_path="" | |
| fi | |
| # Check if SSH Host exists | |
| if ! check_remote_exists "$host_alias"; then | |
| exit 1 | |
| fi | |
| # Ensure log directory exists | |
| sudo mkdir -p "$LOG_DIR" | |
| # Execute corresponding operation | |
| print_message "INFO" "Operation: $operation, log: $LOG_FILE" | |
| case "$operation" in | |
| "mount"|"mnt") | |
| mount_remote "$host_alias" "$remote_path" "$custom_mount_point" | |
| ;; | |
| "unmount"|"umount"|"umnt") | |
| unmount_remote "$host_alias" "$custom_mount_point" | |
| ;; | |
| "status") | |
| check_status "$host_alias" "$remote_path" | |
| ;; | |
| *) | |
| print_message "ERROR" "Unknown operation: $operation" | |
| show_help | |
| exit 1 | |
| ;; | |
| esac | |
| } | |
| # Execute main function | |
| main "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| sclone - 輕量 rclone SFTP 包裹器 (PowerShell 版本) | |
| .DESCRIPTION | |
| 透過讀取 SSH config 動態建立 SFTP backend | |
| 支援 ProxyJump 和多 remote 操作 | |
| .PARAMETER RcloneCommand | |
| rclone 命令 (如: ls, copy, sync, cat 等) | |
| .PARAMETER Args | |
| 傳遞給 rclone 的其餘參數 | |
| .EXAMPLE | |
| .\sclone.ps1 ls mom: | |
| 列出 mom 根目錄 | |
| .EXAMPLE | |
| .\sclone.ps1 ls mom:/WEB | |
| 列出 mom:/WEB | |
| .EXAMPLE | |
| .\sclone.ps1 cat mom:/file.txt | |
| 顯示檔案內容 | |
| .EXAMPLE | |
| .\sclone.ps1 copy mom:/src /local/dest | |
| 複製到本地 | |
| .EXAMPLE | |
| .\sclone.ps1 sync /local/src mom:/dest | |
| 同步到遠端 | |
| .EXAMPLE | |
| .\sclone.ps1 copy host1:/src host2:/dest | |
| 多 remote 複製(無 proxy 限定) | |
| .NOTES | |
| 版本: 1.0.0 | |
| 日期: 2026-01-11 | |
| 作者: Translated from Bash version | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Position = 0)] | |
| [string]$RcloneCommand, | |
| [Parameter(Position = 1, ValueFromRemainingArguments)] | |
| [string[]]$RcloneArgs | |
| ) | |
| # ==================== 錯誤處理設定 ==================== | |
| $ErrorActionPreference = 'Stop' | |
| Set-StrictMode -Version Latest | |
| # ==================== 全域設定 ==================== | |
| $script:Config = @{ | |
| SSHConfig = if ($env:SCLONE_SSH_CONFIG) { $env:SCLONE_SSH_CONFIG } else { "$env:USERPROFILE\.ssh\config" } | |
| } | |
| # 基本 SSH 命令模板 | |
| $script:SSHCmdBase = "ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%r@%h:%p -o ControlPersist=10 -o ConnectTimeout=2" | |
| # 用於追蹤需要清理的 hosts | |
| $script:CleanupHosts = @() | |
| # ==================== 輔助函數: 訊息輸出 ==================== | |
| <# | |
| .SYNOPSIS | |
| 輸出帶顏色的訊息 | |
| #> | |
| function Write-ColorMessage { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')] | |
| [string]$Level, | |
| [Parameter(Mandatory)] | |
| [string]$Message | |
| ) | |
| $color = switch ($Level) { | |
| 'INFO' { 'Green' } | |
| 'WARN' { 'Yellow' } | |
| 'ERROR' { 'Red' } | |
| 'DEBUG' { 'Cyan' } | |
| } | |
| Write-Host "[$Level] $Message" -ForegroundColor $color | |
| } | |
| # ==================== SSH Config 解析函數 ==================== | |
| <# | |
| .SYNOPSIS | |
| 解析 SSH config 獲取指定 Host 的指定屬性 | |
| #> | |
| function Get-SSHConfigValue { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter(Mandatory)] | |
| [string]$Key | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return $null | |
| } | |
| $content = Get-Content $sshConfigPath -Raw | |
| $lines = $content -split "`r?`n" | |
| $inHostBlock = $false | |
| foreach ($line in $lines) { | |
| # 檢查是否進入目標 Host 區塊 | |
| if ($line -match '^\s*Host\s+(.+)$') { | |
| $hostPatterns = $Matches[1] -split '\s+' | |
| $inHostBlock = $hostPatterns -contains $HostAlias | |
| continue | |
| } | |
| # 如果在目標 Host 區塊內,查找屬性 | |
| if ($inHostBlock) { | |
| if ($line -match "^\s*$Key\s+(.+)$") { | |
| return $Matches[1].Trim() | |
| } | |
| # 遇到下一個 Host 區塊,停止搜尋 | |
| if ($line -match '^\s*Host\s+') { | |
| break | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| <# | |
| .SYNOPSIS | |
| 獲取 SSH Host 的完整設定 | |
| #> | |
| function Get-SSHHostConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $hostname = Get-SSHConfigValue -HostAlias $HostAlias -Key 'HostName' | |
| $user = Get-SSHConfigValue -HostAlias $HostAlias -Key 'User' | |
| $port = Get-SSHConfigValue -HostAlias $HostAlias -Key 'Port' | |
| $identityFile = Get-SSHConfigValue -HostAlias $HostAlias -Key 'IdentityFile' | |
| $proxyJump = Get-SSHConfigValue -HostAlias $HostAlias -Key 'ProxyJump' | |
| # 如果沒有明確的 HostName,使用 Host alias | |
| if (-not $hostname) { | |
| $hostname = $HostAlias | |
| } | |
| # 展開 ~ 為實際路徑 | |
| if ($identityFile -and $identityFile.StartsWith('~')) { | |
| $identityFile = $identityFile -replace '^~', $env:USERPROFILE | |
| } | |
| return [PSCustomObject]@{ | |
| HostAlias = $HostAlias | |
| HostName = $hostname | |
| User = $user | |
| Port = $port | |
| IdentityFile = $identityFile | |
| ProxyJump = $proxyJump | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 檢查 host 是否需要 proxy | |
| #> | |
| function Test-HasProxy { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $proxyJump = Get-SSHConfigValue -HostAlias $HostAlias -Key 'ProxyJump' | |
| return [bool]$proxyJump | |
| } | |
| <# | |
| .SYNOPSIS | |
| 組建完整 SSH 命令字串(用於 --sftp-ssh 模式) | |
| #> | |
| function Get-SSHCommand { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $config = Get-SSHHostConfig -HostAlias $HostAlias | |
| $sshCmd = $script:SSHCmdBase | |
| # 添加 Port | |
| if ($config.Port) { | |
| $sshCmd += " -p $($config.Port)" | |
| } | |
| # 添加 IdentityFile | |
| if ($config.IdentityFile) { | |
| $sshCmd += " -i `"$($config.IdentityFile)`"" | |
| } | |
| # 添加 ProxyJump | |
| if ($config.ProxyJump) { | |
| $sshCmd += " -J $($config.ProxyJump)" | |
| } | |
| # 組建完整的 SSH 連線字串 user@hostname | |
| $sshTarget = if ($config.User) { | |
| "$($config.User)@$($config.HostName)" | |
| } else { | |
| $config.HostName | |
| } | |
| return "$sshCmd $sshTarget" | |
| } | |
| # ==================== Host 識別函數 ==================== | |
| <# | |
| .SYNOPSIS | |
| 檢查 host 是否存在於 SSH config | |
| #> | |
| function Test-HostExistsInSSHConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return $false | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $found = $content | Where-Object { $_ -match "^\s*Host\s+.*\b$([regex]::Escape($HostAlias))\b" } | |
| return [bool]$found | |
| } | |
| <# | |
| .SYNOPSIS | |
| 獲取可用的 SSH Host 清單 | |
| #> | |
| function Get-AvailableHosts { | |
| [CmdletBinding()] | |
| param() | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return @() | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $hosts = $content | Where-Object { $_ -match '^\s*Host\s+(.+)$' } | ForEach-Object { | |
| $Matches[1] -split '\s+' | Where-Object { $_ -notmatch '\*' -and $_ -ne 'github.com' } | |
| } | Select-Object -Unique | Sort-Object | |
| return $hosts | |
| } | |
| <# | |
| .SYNOPSIS | |
| 從參數中識別 SSH host | |
| 輸出格式: 唯一的 host alias 陣列 | |
| #> | |
| function Find-SSHHostsInArgs { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string[]]$InputArgs | |
| ) | |
| $foundHosts = @() | |
| foreach ($arg in $InputArgs) { | |
| # 檢查是否符合 xxx: 或 xxx:/path 格式 | |
| if ($arg -match '^([a-zA-Z0-9_-]+):') { | |
| $potentialHost = $Matches[1] | |
| # 檢查是否存在於 SSH config | |
| if (Test-HostExistsInSSHConfig -HostAlias $potentialHost) { | |
| $foundHosts += $potentialHost | |
| } | |
| } | |
| } | |
| # 回傳去重後的 hosts | |
| return $foundHosts | Select-Object -Unique | |
| } | |
| # ==================== rclone Config 建立函數 ==================== | |
| <# | |
| .SYNOPSIS | |
| 為無 proxy 的 host 建立原生 SFTP 設定 | |
| #> | |
| function New-NativeSftpConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $config = Get-SSHHostConfig -HostAlias $HostAlias | |
| # 建立設定參數 | |
| $configArgs = @($HostAlias, 'sftp', "host=$($config.HostName)") | |
| if ($config.User) { | |
| $configArgs += "user=$($config.User)" | |
| } | |
| if ($config.Port) { | |
| $configArgs += "port=$($config.Port)" | |
| } | |
| if ($config.IdentityFile) { | |
| $configArgs += "key_file=$($config.IdentityFile)" | |
| } | |
| try { | |
| $result = & rclone config create @configArgs --non-interactive 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $result" | |
| return $false | |
| } | |
| return $true | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $_" | |
| return $false | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| 為有 proxy 的 host 建立使用 ssh 選項的設定 | |
| #> | |
| function New-SSHSftpConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $sshCmd = Get-SSHCommand -HostAlias $HostAlias | |
| try { | |
| $result = & rclone config create $HostAlias sftp "ssh=$sshCmd" --non-interactive 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $result" | |
| return $false | |
| } | |
| return $true | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $_" | |
| return $false | |
| } | |
| } | |
| # ==================== 清理函數 ==================== | |
| <# | |
| .SYNOPSIS | |
| 清理所有已建立的 rclone config | |
| #> | |
| function Invoke-Cleanup { | |
| [CmdletBinding()] | |
| param() | |
| foreach ($hostItem in $script:CleanupHosts) { | |
| try { | |
| & rclone config delete $hostItem 2>&1 | Out-Null | |
| } catch { | |
| # 忽略清理錯誤 | |
| } | |
| } | |
| } | |
| # ==================== 使用說明 ==================== | |
| <# | |
| .SYNOPSIS | |
| 顯示使用說明 | |
| #> | |
| function Show-Help { | |
| [CmdletBinding()] | |
| param() | |
| Write-Host "sclone - 輕量 rclone SFTP 包裹器" -ForegroundColor Cyan | |
| Write-Host "" | |
| Write-Host "使用方式:" -ForegroundColor Yellow | |
| Write-Host " .\sclone.ps1 <rclone-command> <host_alias>[:/path] [args...]" | |
| Write-Host "" | |
| Write-Host "說明:" -ForegroundColor Yellow | |
| Write-Host " 透過讀取 SSH config 自動建立 SFTP 連線,支援 SSH 所有原生參數。" | |
| Write-Host "" | |
| Write-Host "範例:" -ForegroundColor Yellow | |
| Write-Host " .\sclone.ps1 ls mom: # 列出 mom 根目錄" | |
| Write-Host " .\sclone.ps1 ls mom:/WEB # 列出 mom:/WEB" | |
| Write-Host " .\sclone.ps1 cat mom:/file.txt # 顯示檔案內容" | |
| Write-Host " .\sclone.ps1 copy mom:/src C:\local\dest # 複製到本地" | |
| Write-Host " .\sclone.ps1 sync C:\local\src mom:/dest # 同步到遠端" | |
| Write-Host " .\sclone.ps1 copy host1:/src host2:/dest # 多 remote 複製(無 proxy 限定)" | |
| Write-Host "" | |
| Write-Host "環境變數:" -ForegroundColor Yellow | |
| Write-Host " SCLONE_SSH_CONFIG SSH 設定檔路徑 (預設: ~\.ssh\config)" | |
| Write-Host "" | |
| Write-Host "注意:" -ForegroundColor Yellow | |
| Write-Host " - 支援多 remote 操作(僅限無 ProxyJump 的 host)" | |
| Write-Host " - 若任一目標 host 有 ProxyJump 設定,僅支援單一 remote" | |
| Write-Host "" | |
| Write-Host "可用的 SSH Hosts:" -ForegroundColor Yellow | |
| $availableHosts = Get-AvailableHosts | |
| if ($availableHosts.Count -gt 0) { | |
| Write-Host " $($availableHosts -join ', ')" | |
| } else { | |
| Write-Host " (無可用的 SSH Host)" | |
| } | |
| } | |
| # ==================== 主函數 ==================== | |
| function Main { | |
| # 無參數或請求幫助 | |
| if (-not $RcloneCommand -or $RcloneCommand -in @('-h', '--help', 'help')) { | |
| Show-Help | |
| exit 0 | |
| } | |
| # 檢查 SSH config 是否存在 | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| Write-ColorMessage -Level ERROR -Message "SSH config 不存在: $sshConfigPath" | |
| exit 1 | |
| } | |
| # 識別參數中的 SSH hosts | |
| $allArgs = @($RcloneCommand) + @($RcloneArgs) | |
| if (-not $RcloneArgs -or $RcloneArgs.Count -eq 0) { | |
| Write-ColorMessage -Level ERROR -Message "參數中未找到有效的 SSH Host" | |
| $availableHosts = Get-AvailableHosts | |
| Write-Host "可用的 SSH Hosts: $($availableHosts -join ', ')" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| $sshHosts = @(Find-SSHHostsInArgs -InputArgs $RcloneArgs) | |
| # 檢查是否找到 SSH host | |
| if ($sshHosts.Count -eq 0) { | |
| Write-ColorMessage -Level ERROR -Message "參數中未找到有效的 SSH Host" | |
| $availableHosts = Get-AvailableHosts | |
| Write-Host "可用的 SSH Hosts: $($availableHosts -join ', ')" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| # 計算找到的 host 數量 | |
| $hostCount = $sshHosts.Count | |
| # 檢測是否有任何 host 需要 proxy | |
| $hasAnyProxy = $false | |
| foreach ($hostItem in $sshHosts) { | |
| if (Test-HasProxy -HostAlias $hostItem) { | |
| $hasAnyProxy = $true | |
| break | |
| } | |
| } | |
| # 多 remote + proxy = 不支援 | |
| if ($hostCount -gt 1 -and $hasAnyProxy) { | |
| Write-ColorMessage -Level ERROR -Message "多 remote 場景不支援 ProxyJump" | |
| Write-Host "偵測到多個 SSH Hosts: $($sshHosts -join ', ')" -ForegroundColor Yellow | |
| Write-Host "請直接使用 rclone 並手動設定 backend" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| # 使用 try/finally 確保清理 | |
| try { | |
| # 建立各 host 的 rclone config | |
| foreach ($hostItem in $sshHosts) { | |
| $script:CleanupHosts += $hostItem | |
| if (Test-HasProxy -HostAlias $hostItem) { | |
| if (-not (New-SSHSftpConfig -HostAlias $hostItem)) { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $hostItem" | |
| exit 1 | |
| } | |
| } else { | |
| if (-not (New-NativeSftpConfig -HostAlias $hostItem)) { | |
| Write-ColorMessage -Level ERROR -Message "建立 rclone config 失敗: $hostItem" | |
| exit 1 | |
| } | |
| } | |
| } | |
| # 執行 rclone 命令 | |
| Write-ColorMessage -Level DEBUG -Message "執行: rclone $RcloneCommand $($RcloneArgs -join ' ')" | |
| & rclone $RcloneCommand @RcloneArgs | |
| $exitCode = $LASTEXITCODE | |
| } finally { | |
| # 清理 rclone config | |
| Invoke-Cleanup | |
| } | |
| exit $exitCode | |
| } | |
| # 執行主程式 | |
| Main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # sclone - 輕量 rclone SFTP 包裹器 | |
| # 透過讀取 ~/.ssh/config 動態建立 SFTP backend | |
| # 使用方式: sclone <rclone-command> <host_alias>[:/path] [args...] | |
| # 範例: sclone ls mom:/WEB | |
| set -euo pipefail | |
| # === 設定 === | |
| SSH_CONFIG="${SCLONE_SSH_CONFIG:-$HOME/.ssh/config}" | |
| # 基本 SSH 命令模板 | |
| SSH_CMD_BASE="ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%r@%h:%p -o ControlPersist=10 -o ConnectTimeout=2" | |
| # 用於追蹤需要清理的 hosts(支援多個) | |
| CLEANUP_HOSTS=() | |
| # === SSH Config 解析函數 === | |
| # 解析 SSH config 獲取指定 Host 的指定屬性 | |
| parse_ssh_config() { | |
| local host_alias="$1" | |
| local key="$2" | |
| awk -v host="$host_alias" -v key="$key" ' | |
| tolower($1) == "host" { | |
| for (i = 2; i <= NF; i++) { | |
| if ($i == host) { | |
| found = 1 | |
| next | |
| } | |
| } | |
| found = 0 | |
| } | |
| found && tolower($1) == tolower(key) { | |
| print $2 | |
| exit | |
| } | |
| ' "$SSH_CONFIG" | |
| } | |
| # 獲取 SSH Host 的完整設定 | |
| # 輸出格式: hostname|user|port|identity_file|proxy_jump | |
| get_ssh_host_config() { | |
| local host_alias="$1" | |
| local hostname user port identity_file proxy_jump | |
| hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| user=$(parse_ssh_config "$host_alias" "User") | |
| port=$(parse_ssh_config "$host_alias" "Port") | |
| identity_file=$(parse_ssh_config "$host_alias" "IdentityFile") | |
| proxy_jump=$(parse_ssh_config "$host_alias" "ProxyJump") | |
| # 如果沒有明確的 HostName,使用 Host alias | |
| hostname="${hostname:-$host_alias}" | |
| # 展開 ~ 為實際路徑 | |
| if [[ -n "$identity_file" ]]; then | |
| identity_file="${identity_file/#\~/$HOME}" | |
| fi | |
| echo "$hostname|$user|$port|$identity_file|$proxy_jump" | |
| } | |
| # 檢測 host 是否需要 proxy | |
| has_proxy() { | |
| local host_alias="$1" | |
| local proxy_jump | |
| proxy_jump=$(parse_ssh_config "$host_alias" "ProxyJump") | |
| [[ -n "$proxy_jump" ]] | |
| } | |
| # 組建完整 SSH 命令字串(用於 --sftp-ssh 模式) | |
| build_ssh_command() { | |
| local host_alias="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| local ssh_cmd="$SSH_CMD_BASE" | |
| config_data=$(get_ssh_host_config "$host_alias") | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # 添加 Port | |
| if [[ -n "$port" ]]; then | |
| ssh_cmd="$ssh_cmd -p $port" | |
| fi | |
| # 添加 IdentityFile | |
| if [[ -n "$key_file" ]]; then | |
| ssh_cmd="$ssh_cmd -i $key_file" | |
| fi | |
| # 添加 ProxyJump | |
| if [[ -n "$proxy_jump" ]]; then | |
| ssh_cmd="$ssh_cmd -J $proxy_jump" | |
| fi | |
| # 組建完整的 SSH 連線字串 user@hostname | |
| local ssh_target="" | |
| if [[ -n "$user" ]]; then | |
| ssh_target="${user}@${hostname}" | |
| else | |
| ssh_target="${hostname}" | |
| fi | |
| echo "$ssh_cmd $ssh_target" | |
| } | |
| # 為無 proxy 的 host 建立原生 SFTP 設定 | |
| create_native_sftp_config() { | |
| local host_alias="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| config_data=$(get_ssh_host_config "$host_alias") | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # 建立設定參數 | |
| local args=("$host_alias" "sftp" "host=$hostname") | |
| [[ -n "$user" ]] && args+=("user=$user") | |
| [[ -n "$port" ]] && args+=("port=$port") | |
| [[ -n "$key_file" ]] && args+=("key_file=$key_file") | |
| rclone config create "${args[@]}" --non-interactive >/dev/null 2>&1 | |
| } | |
| # 為有 proxy 的 host 建立使用 ssh 選項的設定 | |
| create_ssh_sftp_config() { | |
| local host_alias="$1" | |
| local ssh_cmd | |
| ssh_cmd=$(build_ssh_command "$host_alias") | |
| rclone config create "$host_alias" sftp "ssh=$ssh_cmd" --non-interactive >/dev/null 2>&1 | |
| } | |
| # === Host 識別函數 === | |
| # 檢查 host 是否存在於 SSH config | |
| host_exists_in_ssh_config() { | |
| local host_alias="$1" | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| return 1 | |
| fi | |
| grep -q "^Host .*\\b${host_alias}\\b" "$SSH_CONFIG" 2>/dev/null | |
| } | |
| # 獲取可用的 SSH Host 清單 | |
| get_available_hosts() { | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| return 1 | |
| fi | |
| grep "^Host " "$SSH_CONFIG" | awk '{print $2}' | grep -v '\*' | grep -v 'github.com' | sort | tr '\n' ' ' | |
| } | |
| # 從參數中識別 SSH host | |
| # 輸出格式: host_alias (可能有多個,每行一個) | |
| find_ssh_hosts_in_args() { | |
| local args=("$@") | |
| local found_hosts=() | |
| for arg in "${args[@]}"; do | |
| # 檢查是否符合 xxx: 或 xxx:/path 格式 | |
| if [[ "$arg" =~ ^([a-zA-Z0-9_-]+): ]]; then | |
| local potential_host="${BASH_REMATCH[1]}" | |
| # 檢查是否存在於 SSH config | |
| if host_exists_in_ssh_config "$potential_host"; then | |
| found_hosts+=("$potential_host") | |
| fi | |
| fi | |
| done | |
| # 輸出找到的 hosts(去重) | |
| printf '%s\n' "${found_hosts[@]}" | sort -u | |
| } | |
| # === 清理函數 === | |
| cleanup() { | |
| for host in "${CLEANUP_HOSTS[@]}"; do | |
| rclone config delete "$host" 2>/dev/null || true | |
| done | |
| } | |
| # === 使用說明 === | |
| show_help() { | |
| cat << 'EOF' | |
| sclone - 輕量 rclone SFTP 包裹器 | |
| 使用方式: | |
| sclone <rclone-command> <host_alias>[:/path] [args...] | |
| 說明: | |
| 透過讀取 SSH config 自動建立 SFTP 連線,支援 SSH 所有原生參數。 | |
| 範例: | |
| sclone ls mom: # 列出 mom 根目錄 | |
| sclone ls mom:/WEB # 列出 mom:/WEB | |
| sclone cat mom:/file.txt # 顯示檔案內容 | |
| sclone copy mom:/src /local/dest # 複製到本地 | |
| sclone sync /local/src mom:/dest # 同步到遠端 | |
| sclone copy host1:/src host2:/dest # 多 remote 複製(無 proxy 限定) | |
| 環境變數: | |
| SCLONE_SSH_CONFIG SSH 設定檔路徑 (預設: ~/.ssh/config) | |
| 注意: | |
| - 支援多 remote 操作(僅限無 ProxyJump 的 host) | |
| - 若任一目標 host 有 ProxyJump 設定,僅支援單一 remote | |
| EOF | |
| } | |
| # === 主函數 === | |
| main() { | |
| # 無參數或請求幫助 | |
| if [[ $# -eq 0 ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "help" ]]; then | |
| show_help | |
| exit 0 | |
| fi | |
| # 檢查 SSH config 是否存在 | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| echo "錯誤: SSH config 不存在: $SSH_CONFIG" >&2 | |
| exit 1 | |
| fi | |
| # 識別參數中的 SSH hosts | |
| local ssh_hosts | |
| ssh_hosts=$(find_ssh_hosts_in_args "$@") | |
| # 檢查是否找到 SSH host | |
| if [[ -z "$ssh_hosts" ]]; then | |
| echo "錯誤: 參數中未找到有效的 SSH Host" >&2 | |
| echo "可用的 SSH Hosts: $(get_available_hosts)" >&2 | |
| exit 1 | |
| fi | |
| # 計算找到的 host 數量 | |
| local host_count | |
| host_count=$(echo "$ssh_hosts" | wc -l) | |
| # 檢測是否有任何 host 需要 proxy | |
| local has_any_proxy=false | |
| while IFS= read -r host; do | |
| if has_proxy "$host"; then | |
| has_any_proxy=true | |
| break | |
| fi | |
| done <<< "$ssh_hosts" | |
| # 多 remote + proxy = 不支援 | |
| if [[ $host_count -gt 1 ]] && [[ "$has_any_proxy" == "true" ]]; then | |
| echo "錯誤: 多 remote 場景不支援 ProxyJump" >&2 | |
| echo "偵測到多個 SSH Hosts: $(echo "$ssh_hosts" | tr '\n' ' ')" >&2 | |
| echo "請直接使用 rclone 並手動設定 backend" >&2 | |
| exit 1 | |
| fi | |
| # 設定清理 trap | |
| trap cleanup EXIT | |
| # 建立各 host 的 rclone config | |
| while IFS= read -r host; do | |
| CLEANUP_HOSTS+=("$host") | |
| if has_proxy "$host"; then | |
| if ! create_ssh_sftp_config "$host"; then | |
| echo "錯誤: 建立 rclone config 失敗: $host" >&2 | |
| exit 1 | |
| fi | |
| else | |
| if ! create_native_sftp_config "$host"; then | |
| echo "錯誤: 建立 rclone config 失敗: $host" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| done <<< "$ssh_hosts" | |
| # 執行 rclone 命令 | |
| rclone "$@" | |
| } | |
| main "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| sclone - Lightweight rclone SFTP wrapper (PowerShell Version) | |
| .DESCRIPTION | |
| Dynamically creates SFTP backend by reading SSH config | |
| Supports ProxyJump and multi-remote operations | |
| .PARAMETER RcloneCommand | |
| rclone command (e.g.: ls, copy, sync, cat, etc.) | |
| .PARAMETER Args | |
| Remaining arguments to pass to rclone | |
| .EXAMPLE | |
| .\sclone_en.ps1 ls mom: | |
| List mom root directory | |
| .EXAMPLE | |
| .\sclone_en.ps1 ls mom:/WEB | |
| List mom:/WEB | |
| .EXAMPLE | |
| .\sclone_en.ps1 cat mom:/file.txt | |
| Display file contents | |
| .EXAMPLE | |
| .\sclone_en.ps1 copy mom:/src /local/dest | |
| Copy to local | |
| .EXAMPLE | |
| .\sclone_en.ps1 sync /local/src mom:/dest | |
| Sync to remote | |
| .EXAMPLE | |
| .\sclone_en.ps1 copy host1:/src host2:/dest | |
| Multi-remote copy (no proxy only) | |
| .NOTES | |
| Version: 1.0.0 | |
| Date: 2026-01-11 | |
| Author: Translated from Bash version | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Position = 0)] | |
| [string]$RcloneCommand, | |
| [Parameter(Position = 1, ValueFromRemainingArguments)] | |
| [string[]]$RcloneArgs | |
| ) | |
| # ==================== Error Handling Settings ==================== | |
| $ErrorActionPreference = 'Stop' | |
| Set-StrictMode -Version Latest | |
| # ==================== Global Configuration ==================== | |
| $script:Config = @{ | |
| SSHConfig = if ($env:SCLONE_SSH_CONFIG) { $env:SCLONE_SSH_CONFIG } else { "$env:USERPROFILE\.ssh\config" } | |
| } | |
| # Basic SSH command template | |
| $script:SSHCmdBase = "ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%r@%h:%p -o ControlPersist=10 -o ConnectTimeout=2" | |
| # Track hosts that need cleanup | |
| $script:CleanupHosts = @() | |
| # ==================== Helper Functions: Message Output ==================== | |
| <# | |
| .SYNOPSIS | |
| Output colored message | |
| #> | |
| function Write-ColorMessage { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')] | |
| [string]$Level, | |
| [Parameter(Mandatory)] | |
| [string]$Message | |
| ) | |
| $color = switch ($Level) { | |
| 'INFO' { 'Green' } | |
| 'WARN' { 'Yellow' } | |
| 'ERROR' { 'Red' } | |
| 'DEBUG' { 'Cyan' } | |
| } | |
| Write-Host "[$Level] $Message" -ForegroundColor $color | |
| } | |
| # ==================== SSH Config Parsing Functions ==================== | |
| <# | |
| .SYNOPSIS | |
| Parse SSH config to get specified property for a Host | |
| #> | |
| function Get-SSHConfigValue { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias, | |
| [Parameter(Mandatory)] | |
| [string]$Key | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return $null | |
| } | |
| $content = Get-Content $sshConfigPath -Raw | |
| $lines = $content -split "`r?`n" | |
| $inHostBlock = $false | |
| foreach ($line in $lines) { | |
| # Check if entering target Host block | |
| if ($line -match '^\s*Host\s+(.+)$') { | |
| $hostPatterns = $Matches[1] -split '\s+' | |
| $inHostBlock = $hostPatterns -contains $HostAlias | |
| continue | |
| } | |
| # If in target Host block, find property | |
| if ($inHostBlock) { | |
| if ($line -match "^\s*$Key\s+(.+)$") { | |
| return $Matches[1].Trim() | |
| } | |
| # Reached next Host block, stop searching | |
| if ($line -match '^\s*Host\s+') { | |
| break | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| <# | |
| .SYNOPSIS | |
| Get complete SSH Host configuration | |
| #> | |
| function Get-SSHHostConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $hostname = Get-SSHConfigValue -HostAlias $HostAlias -Key 'HostName' | |
| $user = Get-SSHConfigValue -HostAlias $HostAlias -Key 'User' | |
| $port = Get-SSHConfigValue -HostAlias $HostAlias -Key 'Port' | |
| $identityFile = Get-SSHConfigValue -HostAlias $HostAlias -Key 'IdentityFile' | |
| $proxyJump = Get-SSHConfigValue -HostAlias $HostAlias -Key 'ProxyJump' | |
| # If no explicit HostName, use Host alias | |
| if (-not $hostname) { | |
| $hostname = $HostAlias | |
| } | |
| # Expand ~ to actual path | |
| if ($identityFile -and $identityFile.StartsWith('~')) { | |
| $identityFile = $identityFile -replace '^~', $env:USERPROFILE | |
| } | |
| return [PSCustomObject]@{ | |
| HostAlias = $HostAlias | |
| HostName = $hostname | |
| User = $user | |
| Port = $port | |
| IdentityFile = $identityFile | |
| ProxyJump = $proxyJump | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Check if host requires proxy | |
| #> | |
| function Test-HasProxy { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $proxyJump = Get-SSHConfigValue -HostAlias $HostAlias -Key 'ProxyJump' | |
| return [bool]$proxyJump | |
| } | |
| <# | |
| .SYNOPSIS | |
| Build complete SSH command string (for --sftp-ssh mode) | |
| #> | |
| function Get-SSHCommand { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $config = Get-SSHHostConfig -HostAlias $HostAlias | |
| $sshCmd = $script:SSHCmdBase | |
| # Add Port | |
| if ($config.Port) { | |
| $sshCmd += " -p $($config.Port)" | |
| } | |
| # Add IdentityFile | |
| if ($config.IdentityFile) { | |
| $sshCmd += " -i `"$($config.IdentityFile)`"" | |
| } | |
| # Add ProxyJump | |
| if ($config.ProxyJump) { | |
| $sshCmd += " -J $($config.ProxyJump)" | |
| } | |
| # Build complete SSH connection string user@hostname | |
| $sshTarget = if ($config.User) { | |
| "$($config.User)@$($config.HostName)" | |
| } else { | |
| $config.HostName | |
| } | |
| return "$sshCmd $sshTarget" | |
| } | |
| # ==================== Host Identification Functions ==================== | |
| <# | |
| .SYNOPSIS | |
| Check if host exists in SSH config | |
| #> | |
| function Test-HostExistsInSSHConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return $false | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $found = $content | Where-Object { $_ -match "^\s*Host\s+.*\b$([regex]::Escape($HostAlias))\b" } | |
| return [bool]$found | |
| } | |
| <# | |
| .SYNOPSIS | |
| Get available SSH Host list | |
| #> | |
| function Get-AvailableHosts { | |
| [CmdletBinding()] | |
| param() | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| return @() | |
| } | |
| $content = Get-Content $sshConfigPath | |
| $hosts = $content | Where-Object { $_ -match '^\s*Host\s+(.+)$' } | ForEach-Object { | |
| $Matches[1] -split '\s+' | Where-Object { $_ -notmatch '\*' -and $_ -ne 'github.com' } | |
| } | Select-Object -Unique | Sort-Object | |
| return $hosts | |
| } | |
| <# | |
| .SYNOPSIS | |
| Identify SSH hosts from arguments | |
| Output format: unique host alias array | |
| #> | |
| function Find-SSHHostsInArgs { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string[]]$InputArgs | |
| ) | |
| $foundHosts = @() | |
| foreach ($arg in $InputArgs) { | |
| # Check if matches xxx: or xxx:/path format | |
| if ($arg -match '^([a-zA-Z0-9_-]+):') { | |
| $potentialHost = $Matches[1] | |
| # Check if exists in SSH config | |
| if (Test-HostExistsInSSHConfig -HostAlias $potentialHost) { | |
| $foundHosts += $potentialHost | |
| } | |
| } | |
| } | |
| # Return deduplicated hosts | |
| return $foundHosts | Select-Object -Unique | |
| } | |
| # ==================== rclone Config Creation Functions ==================== | |
| <# | |
| .SYNOPSIS | |
| Create native SFTP config for hosts without proxy | |
| #> | |
| function New-NativeSftpConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $config = Get-SSHHostConfig -HostAlias $HostAlias | |
| # Build config parameters | |
| $configArgs = @($HostAlias, 'sftp', "host=$($config.HostName)") | |
| if ($config.User) { | |
| $configArgs += "user=$($config.User)" | |
| } | |
| if ($config.Port) { | |
| $configArgs += "port=$($config.Port)" | |
| } | |
| if ($config.IdentityFile) { | |
| $configArgs += "key_file=$($config.IdentityFile)" | |
| } | |
| try { | |
| $result = & rclone config create @configArgs --non-interactive 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $result" | |
| return $false | |
| } | |
| return $true | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $_" | |
| return $false | |
| } | |
| } | |
| <# | |
| .SYNOPSIS | |
| Create config using ssh option for hosts with proxy | |
| #> | |
| function New-SSHSftpConfig { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$HostAlias | |
| ) | |
| $sshCmd = Get-SSHCommand -HostAlias $HostAlias | |
| try { | |
| $result = & rclone config create $HostAlias sftp "ssh=$sshCmd" --non-interactive 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $result" | |
| return $false | |
| } | |
| return $true | |
| } catch { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $_" | |
| return $false | |
| } | |
| } | |
| # ==================== Cleanup Function ==================== | |
| <# | |
| .SYNOPSIS | |
| Clean up all created rclone configs | |
| #> | |
| function Invoke-Cleanup { | |
| [CmdletBinding()] | |
| param() | |
| foreach ($hostItem in $script:CleanupHosts) { | |
| try { | |
| & rclone config delete $hostItem 2>&1 | Out-Null | |
| } catch { | |
| # Ignore cleanup errors | |
| } | |
| } | |
| } | |
| # ==================== Usage Help ==================== | |
| <# | |
| .SYNOPSIS | |
| Show usage help | |
| #> | |
| function Show-Help { | |
| [CmdletBinding()] | |
| param() | |
| Write-Host "sclone - Lightweight rclone SFTP wrapper" -ForegroundColor Cyan | |
| Write-Host "" | |
| Write-Host "Usage:" -ForegroundColor Yellow | |
| Write-Host " .\sclone_en.ps1 <rclone-command> <host_alias>[:/path] [args...]" | |
| Write-Host "" | |
| Write-Host "Description:" -ForegroundColor Yellow | |
| Write-Host " Automatically creates SFTP connection by reading SSH config, supports all native SSH parameters." | |
| Write-Host "" | |
| Write-Host "Examples:" -ForegroundColor Yellow | |
| Write-Host " .\sclone_en.ps1 ls mom: # List mom root directory" | |
| Write-Host " .\sclone_en.ps1 ls mom:/WEB # List mom:/WEB" | |
| Write-Host " .\sclone_en.ps1 cat mom:/file.txt # Display file contents" | |
| Write-Host " .\sclone_en.ps1 copy mom:/src C:\local\dest # Copy to local" | |
| Write-Host " .\sclone_en.ps1 sync C:\local\src mom:/dest # Sync to remote" | |
| Write-Host " .\sclone_en.ps1 copy host1:/src host2:/dest # Multi-remote copy (no proxy only)" | |
| Write-Host "" | |
| Write-Host "Environment variables:" -ForegroundColor Yellow | |
| Write-Host " SCLONE_SSH_CONFIG SSH config file path (default: ~\.ssh\config)" | |
| Write-Host "" | |
| Write-Host "Notes:" -ForegroundColor Yellow | |
| Write-Host " - Supports multi-remote operations (only for hosts without ProxyJump)" | |
| Write-Host " - If any target host has ProxyJump config, only single remote is supported" | |
| Write-Host "" | |
| Write-Host "Available SSH Hosts:" -ForegroundColor Yellow | |
| $availableHosts = Get-AvailableHosts | |
| if ($availableHosts.Count -gt 0) { | |
| Write-Host " $($availableHosts -join ', ')" | |
| } else { | |
| Write-Host " (No available SSH Hosts)" | |
| } | |
| } | |
| # ==================== Main Function ==================== | |
| function Main { | |
| # No arguments or help requested | |
| if (-not $RcloneCommand -or $RcloneCommand -in @('-h', '--help', 'help')) { | |
| Show-Help | |
| exit 0 | |
| } | |
| # Check if SSH config exists | |
| $sshConfigPath = $script:Config.SSHConfig | |
| if (-not (Test-Path $sshConfigPath)) { | |
| Write-ColorMessage -Level ERROR -Message "SSH config does not exist: $sshConfigPath" | |
| exit 1 | |
| } | |
| # Identify SSH hosts in arguments | |
| $allArgs = @($RcloneCommand) + @($RcloneArgs) | |
| if (-not $RcloneArgs -or $RcloneArgs.Count -eq 0) { | |
| Write-ColorMessage -Level ERROR -Message "No valid SSH Host found in arguments" | |
| $availableHosts = Get-AvailableHosts | |
| Write-Host "Available SSH Hosts: $($availableHosts -join ', ')" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| $sshHosts = @(Find-SSHHostsInArgs -InputArgs $RcloneArgs) | |
| # Check if SSH host found | |
| if ($sshHosts.Count -eq 0) { | |
| Write-ColorMessage -Level ERROR -Message "No valid SSH Host found in arguments" | |
| $availableHosts = Get-AvailableHosts | |
| Write-Host "Available SSH Hosts: $($availableHosts -join ', ')" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| # Count number of hosts found | |
| $hostCount = $sshHosts.Count | |
| # Detect if any host requires proxy | |
| $hasAnyProxy = $false | |
| foreach ($hostItem in $sshHosts) { | |
| if (Test-HasProxy -HostAlias $hostItem) { | |
| $hasAnyProxy = $true | |
| break | |
| } | |
| } | |
| # Multiple remotes + proxy = not supported | |
| if ($hostCount -gt 1 -and $hasAnyProxy) { | |
| Write-ColorMessage -Level ERROR -Message "Multi-remote scenario does not support ProxyJump" | |
| Write-Host "Detected multiple SSH Hosts: $($sshHosts -join ', ')" -ForegroundColor Yellow | |
| Write-Host "Please use rclone directly and configure backend manually" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| # Use try/finally to ensure cleanup | |
| try { | |
| # Create rclone config for each host | |
| foreach ($hostItem in $sshHosts) { | |
| $script:CleanupHosts += $hostItem | |
| if (Test-HasProxy -HostAlias $hostItem) { | |
| if (-not (New-SSHSftpConfig -HostAlias $hostItem)) { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $hostItem" | |
| exit 1 | |
| } | |
| } else { | |
| if (-not (New-NativeSftpConfig -HostAlias $hostItem)) { | |
| Write-ColorMessage -Level ERROR -Message "Failed to create rclone config: $hostItem" | |
| exit 1 | |
| } | |
| } | |
| } | |
| # Execute rclone command | |
| Write-ColorMessage -Level DEBUG -Message "Executing: rclone $RcloneCommand $($RcloneArgs -join ' ')" | |
| & rclone $RcloneCommand @RcloneArgs | |
| $exitCode = $LASTEXITCODE | |
| } finally { | |
| # Clean up rclone config | |
| Invoke-Cleanup | |
| } | |
| exit $exitCode | |
| } | |
| # Execute main program | |
| Main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # sclone - Lightweight rclone SFTP wrapper | |
| # Dynamically creates SFTP backend by reading ~/.ssh/config | |
| # Usage: sclone <rclone-command> <host_alias>[:/path] [args...] | |
| # Example: sclone ls mom:/WEB | |
| set -euo pipefail | |
| # === Configuration === | |
| SSH_CONFIG="${SCLONE_SSH_CONFIG:-$HOME/.ssh/config}" | |
| # Basic SSH command template | |
| SSH_CMD_BASE="ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%r@%h:%p -o ControlPersist=10 -o ConnectTimeout=2" | |
| # Track hosts that need cleanup (supports multiple) | |
| CLEANUP_HOSTS=() | |
| # === SSH Config parsing functions === | |
| # Parse SSH config to get specified property for a Host | |
| parse_ssh_config() { | |
| local host_alias="$1" | |
| local key="$2" | |
| awk -v host="$host_alias" -v key="$key" ' | |
| tolower($1) == "host" { | |
| for (i = 2; i <= NF; i++) { | |
| if ($i == host) { | |
| found = 1 | |
| next | |
| } | |
| } | |
| found = 0 | |
| } | |
| found && tolower($1) == tolower(key) { | |
| print $2 | |
| exit | |
| } | |
| ' "$SSH_CONFIG" | |
| } | |
| # Get complete SSH Host configuration | |
| # Output format: hostname|user|port|identity_file|proxy_jump | |
| get_ssh_host_config() { | |
| local host_alias="$1" | |
| local hostname user port identity_file proxy_jump | |
| hostname=$(parse_ssh_config "$host_alias" "HostName") | |
| user=$(parse_ssh_config "$host_alias" "User") | |
| port=$(parse_ssh_config "$host_alias" "Port") | |
| identity_file=$(parse_ssh_config "$host_alias" "IdentityFile") | |
| proxy_jump=$(parse_ssh_config "$host_alias" "ProxyJump") | |
| # If no explicit HostName, use Host alias | |
| hostname="${hostname:-$host_alias}" | |
| # Expand ~ to actual path | |
| if [[ -n "$identity_file" ]]; then | |
| identity_file="${identity_file/#\~/$HOME}" | |
| fi | |
| echo "$hostname|$user|$port|$identity_file|$proxy_jump" | |
| } | |
| # Detect if host requires proxy | |
| has_proxy() { | |
| local host_alias="$1" | |
| local proxy_jump | |
| proxy_jump=$(parse_ssh_config "$host_alias" "ProxyJump") | |
| [[ -n "$proxy_jump" ]] | |
| } | |
| # Build complete SSH command string (for --sftp-ssh mode) | |
| build_ssh_command() { | |
| local host_alias="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| local ssh_cmd="$SSH_CMD_BASE" | |
| config_data=$(get_ssh_host_config "$host_alias") | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # Add Port | |
| if [[ -n "$port" ]]; then | |
| ssh_cmd="$ssh_cmd -p $port" | |
| fi | |
| # Add IdentityFile | |
| if [[ -n "$key_file" ]]; then | |
| ssh_cmd="$ssh_cmd -i $key_file" | |
| fi | |
| # Add ProxyJump | |
| if [[ -n "$proxy_jump" ]]; then | |
| ssh_cmd="$ssh_cmd -J $proxy_jump" | |
| fi | |
| # Build complete SSH connection string user@hostname | |
| local ssh_target="" | |
| if [[ -n "$user" ]]; then | |
| ssh_target="${user}@${hostname}" | |
| else | |
| ssh_target="${hostname}" | |
| fi | |
| echo "$ssh_cmd $ssh_target" | |
| } | |
| # Create native SFTP config for hosts without proxy | |
| create_native_sftp_config() { | |
| local host_alias="$1" | |
| local config_data hostname user port key_file proxy_jump | |
| config_data=$(get_ssh_host_config "$host_alias") | |
| IFS='|' read -r hostname user port key_file proxy_jump <<< "$config_data" | |
| # Build config parameters | |
| local args=("$host_alias" "sftp" "host=$hostname") | |
| [[ -n "$user" ]] && args+=("user=$user") | |
| [[ -n "$port" ]] && args+=("port=$port") | |
| [[ -n "$key_file" ]] && args+=("key_file=$key_file") | |
| rclone config create "${args[@]}" --non-interactive >/dev/null 2>&1 | |
| } | |
| # Create config using ssh option for hosts with proxy | |
| create_ssh_sftp_config() { | |
| local host_alias="$1" | |
| local ssh_cmd | |
| ssh_cmd=$(build_ssh_command "$host_alias") | |
| rclone config create "$host_alias" sftp "ssh=$ssh_cmd" --non-interactive >/dev/null 2>&1 | |
| } | |
| # === Host identification functions === | |
| # Check if host exists in SSH config | |
| host_exists_in_ssh_config() { | |
| local host_alias="$1" | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| return 1 | |
| fi | |
| grep -q "^Host .*\\b${host_alias}\\b" "$SSH_CONFIG" 2>/dev/null | |
| } | |
| # Get available SSH Host list | |
| get_available_hosts() { | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| return 1 | |
| fi | |
| grep "^Host " "$SSH_CONFIG" | awk '{print $2}' | grep -v '\*' | grep -v 'github.com' | sort | tr '\n' ' ' | |
| } | |
| # Identify SSH hosts from arguments | |
| # Output format: host_alias (possibly multiple, one per line) | |
| find_ssh_hosts_in_args() { | |
| local args=("$@") | |
| local found_hosts=() | |
| for arg in "${args[@]}"; do | |
| # Check if matches xxx: or xxx:/path format | |
| if [[ "$arg" =~ ^([a-zA-Z0-9_-]+): ]]; then | |
| local potential_host="${BASH_REMATCH[1]}" | |
| # Check if exists in SSH config | |
| if host_exists_in_ssh_config "$potential_host"; then | |
| found_hosts+=("$potential_host") | |
| fi | |
| fi | |
| done | |
| # Output found hosts (deduplicated) | |
| printf '%s\n' "${found_hosts[@]}" | sort -u | |
| } | |
| # === Cleanup function === | |
| cleanup() { | |
| for host in "${CLEANUP_HOSTS[@]}"; do | |
| rclone config delete "$host" 2>/dev/null || true | |
| done | |
| } | |
| # === Usage help === | |
| show_help() { | |
| cat << 'EOF' | |
| sclone - Lightweight rclone SFTP wrapper | |
| Usage: | |
| sclone <rclone-command> <host_alias>[:/path] [args...] | |
| Description: | |
| Automatically creates SFTP connection by reading SSH config, supports all native SSH parameters. | |
| Examples: | |
| sclone ls mom: # List mom root directory | |
| sclone ls mom:/WEB # List mom:/WEB | |
| sclone cat mom:/file.txt # Display file contents | |
| sclone copy mom:/src /local/dest # Copy to local | |
| sclone sync /local/src mom:/dest # Sync to remote | |
| sclone copy host1:/src host2:/dest # Multi-remote copy (no proxy only) | |
| Environment variables: | |
| SCLONE_SSH_CONFIG SSH config file path (default: ~/.ssh/config) | |
| Notes: | |
| - Supports multi-remote operations (only for hosts without ProxyJump) | |
| - If any target host has ProxyJump config, only single remote is supported | |
| EOF | |
| } | |
| # === Main function === | |
| main() { | |
| # No arguments or help requested | |
| if [[ $# -eq 0 ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "help" ]]; then | |
| show_help | |
| exit 0 | |
| fi | |
| # Check if SSH config exists | |
| if [[ ! -f "$SSH_CONFIG" ]]; then | |
| echo "Error: SSH config does not exist: $SSH_CONFIG" >&2 | |
| exit 1 | |
| fi | |
| # Identify SSH hosts in arguments | |
| local ssh_hosts | |
| ssh_hosts=$(find_ssh_hosts_in_args "$@") | |
| # Check if SSH host found | |
| if [[ -z "$ssh_hosts" ]]; then | |
| echo "Error: No valid SSH Host found in arguments" >&2 | |
| echo "Available SSH Hosts: $(get_available_hosts)" >&2 | |
| exit 1 | |
| fi | |
| # Count number of hosts found | |
| local host_count | |
| host_count=$(echo "$ssh_hosts" | wc -l) | |
| # Detect if any host requires proxy | |
| local has_any_proxy=false | |
| while IFS= read -r host; do | |
| if has_proxy "$host"; then | |
| has_any_proxy=true | |
| break | |
| fi | |
| done <<< "$ssh_hosts" | |
| # Multiple remotes + proxy = not supported | |
| if [[ $host_count -gt 1 ]] && [[ "$has_any_proxy" == "true" ]]; then | |
| echo "Error: Multi-remote scenario does not support ProxyJump" >&2 | |
| echo "Detected multiple SSH Hosts: $(echo "$ssh_hosts" | tr '\n' ' ')" >&2 | |
| echo "Please use rclone directly and configure backend manually" >&2 | |
| exit 1 | |
| fi | |
| # Set cleanup trap | |
| trap cleanup EXIT | |
| # Create rclone config for each host | |
| while IFS= read -r host; do | |
| CLEANUP_HOSTS+=("$host") | |
| if has_proxy "$host"; then | |
| if ! create_ssh_sftp_config "$host"; then | |
| echo "Error: Failed to create rclone config: $host" >&2 | |
| exit 1 | |
| fi | |
| else | |
| if ! create_native_sftp_config "$host"; then | |
| echo "Error: Failed to create rclone config: $host" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| done <<< "$ssh_hosts" | |
| # Execute rclone command | |
| rclone "$@" | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment