Skip to content

Commit a61f10f

Browse files
committed
feat(rclone): 支持从 JSON 配置 WebUI 密码
WebUI 密码属于 rclone RC 启动参数,不适合作为 remote section 写入生成的 rclone.conf。运维 JSON 增加 webui section,脚本按命令行、环境变量、JSON、默认值的优先级解析 addr/user/pass,并支持 pass 使用环境变量占位符。 Constraint: 不把真实密码写入示例,保留 RCLONE_RC_PASS 注入方式 Rejected: 写入 rclone.conf remote 段 | rc-pass 不是 remote backend 配置 Confidence: high Scope-risk: narrow Directive: WebUI/RC 启动参数应放在运维 JSON 的 webui section 或通过 flag/env 注入,不要混入 remote 配置段 Tested: Pester RcloneOps.Tests.ps1 Tested: node --test config/service/oss/rclone/rclone-ops.test.mjs Tested: pnpm qa Tested: pnpm test:pwsh:all Not-tested: 真实 rclone WebUI 交互登录
1 parent e79377b commit a61f10f

6 files changed

Lines changed: 292 additions & 11 deletions

File tree

config/service/oss/rclone/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
2. 每个 remote 必须包含 `name``type`
2323
3. remote 对象中除 `name` 外的字段会按原键名写入 `rclone.conf`
2424
4. 字符串值可使用 `${ENV_VAR}` 占位符,从当前进程环境变量读取密钥;缺失变量会直接报错。
25+
5. 可选的 `webui` section 可配置 `addr``user``pass`,命令行参数与环境变量优先级高于 JSON。
2526

2627
示例:
2728

@@ -47,7 +48,12 @@
4748
"endpoint": "http://127.0.0.1:9000",
4849
"force_path_style": "true"
4950
}
50-
]
51+
],
52+
"webui": {
53+
"addr": "127.0.0.1:5572",
54+
"user": "admin",
55+
"pass": "${RCLONE_RC_PASS}"
56+
}
5157
}
5258
```
5359

@@ -104,7 +110,7 @@ pwsh ./rclone-ops.ps1 doctor
104110

105111
默认监听 `127.0.0.1:5572`。不带 `--background` 时是前台运行模式,命令会持续占用当前终端,并把 rclone 日志直接显示在当前终端;可打开 `http://127.0.0.1:5572` 确认状态,按 `Ctrl+C` 停止。后台模式才会把日志写入 `.runtime/logs/webui.log`
106112

107-
如果未设置 `RCLONE_RC_PASS`rclone WebUI 会自动生成临时认证信息;建议日常运维显式设置强密码:
113+
WebUI 密码可以通过三种方式设置,优先级为命令行 `--pass` > 环境变量 `RCLONE_RC_PASS` > JSON `webui.pass`。如果 JSON 中写了 `${RCLONE_RC_PASS}`,运行前需要导出该环境变量;如果三者都未设置,rclone WebUI 会自动生成临时认证信息
108114

109115
```bash
110116
RCLONE_RC_PASS='强密码' pwsh ./rclone-ops.ps1 webui

config/service/oss/rclone/rclone-ops.mjs

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,70 @@ export function resolveEnvPlaceholders(value, context = 'config') {
127127
})
128128
}
129129

130+
/**
131+
* 安全读取可选 JSON 主配置文件。
132+
*
133+
* @param {string} path 配置文件路径。
134+
* @returns {Record<string, unknown>} 文件存在时返回 JSON 顶层配置,否则返回空对象。
135+
*/
136+
function readOptionalConfigValues(path) {
137+
if (!existsSync(path)) {
138+
return {}
139+
}
140+
return readConfigValues(path)
141+
}
142+
143+
/**
144+
* 读取嵌套 JSON 配置值。
145+
*
146+
* @param {Record<string, unknown>} values 顶层 JSON 配置对象。
147+
* @param {string} section section 名称。
148+
* @param {string} name section 内部键名。
149+
* @returns {unknown} 命中的配置值;未命中时返回 undefined。
150+
*/
151+
function getNestedConfigValue(values, section, name) {
152+
const sectionValue = values?.[section]
153+
if (!sectionValue || typeof sectionValue !== 'object' || Array.isArray(sectionValue)) {
154+
return undefined
155+
}
156+
return sectionValue[name]
157+
}
158+
159+
/**
160+
* 按 flag、环境变量、JSON 配置、默认值的优先级解析选项。
161+
*
162+
* @param {Map<string, string|boolean>} flags 命令行 flag 集合。
163+
* @param {string} name flag 名称。
164+
* @param {string} envName 环境变量名称。
165+
* @param {Record<string, unknown>} configValues 顶层 JSON 配置对象。
166+
* @param {string} section JSON section 名称。
167+
* @param {string} configName JSON section 内部键名。
168+
* @param {string} fallback 默认值。
169+
* @returns {string} 解析后的字符串值。
170+
*/
171+
export function resolveOptionWithConfig(
172+
flags,
173+
name,
174+
envName,
175+
configValues,
176+
section,
177+
configName,
178+
fallback,
179+
) {
180+
const flagValue = flags.get(name)
181+
if (typeof flagValue === 'string' && flagValue.length > 0) {
182+
return flagValue
183+
}
184+
if (process.env[envName]) {
185+
return process.env[envName]
186+
}
187+
const configValue = getNestedConfigValue(configValues, section, configName)
188+
if (configValue !== undefined && String(configValue).length > 0) {
189+
return String(resolveEnvPlaceholders(configValue, `${section}.${configName}`))
190+
}
191+
return fallback
192+
}
193+
130194
/**
131195
* 根据 JSON remotes 数组生成 rclone remote 定义。
132196
*
@@ -361,14 +425,42 @@ function resolveRcloneRuntime(flags) {
361425
*/
362426
function commandWebui(flags, passthrough) {
363427
const { binary, configPath } = resolveRcloneRuntime(flags)
364-
const rcAddr = resolveOption(flags, 'addr', 'RCLONE_RC_ADDR', DEFAULT_RC_ADDR)
365-
const rcUser = resolveOption(flags, 'user', 'RCLONE_RC_USER', DEFAULT_RC_USER)
366-
const rcPass = resolveOption(
428+
const sourcePath = resolveOption(
429+
flags,
430+
'source',
431+
'RCLONE_SOURCE_CONFIG_PATH',
432+
DEFAULT_SOURCE_PATH,
433+
)
434+
const sourceValues = readOptionalConfigValues(sourcePath)
435+
const rcAddr = resolveOptionWithConfig(
436+
flags,
437+
'addr',
438+
'RCLONE_RC_ADDR',
439+
sourceValues,
440+
'webui',
441+
'addr',
442+
DEFAULT_RC_ADDR,
443+
)
444+
const rcPass = resolveOptionWithConfig(
367445
flags,
368446
'pass',
369447
'RCLONE_RC_PASS',
370-
process.env.RCLONE_RC_PASS ?? '',
448+
sourceValues,
449+
'webui',
450+
'pass',
451+
'',
371452
)
453+
const rcUser = rcPass
454+
? resolveOptionWithConfig(
455+
flags,
456+
'user',
457+
'RCLONE_RC_USER',
458+
sourceValues,
459+
'webui',
460+
'user',
461+
DEFAULT_RC_USER,
462+
)
463+
: ''
372464
const isBackground = flags.has('background')
373465
const logFile = resolveOption(
374466
flags,

config/service/oss/rclone/rclone-ops.ps1

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,146 @@ function Get-RcloneOpsConfigValue {
271271
return $null
272272
}
273273

274+
function Get-RcloneOpsOptionalConfigValues {
275+
<#
276+
.SYNOPSIS
277+
读取可选的 rclone JSON 主配置。
278+
279+
.PARAMETER ConfigPath
280+
JSON 配置文件路径;文件不存在时返回空表。
281+
282+
.OUTPUTS
283+
hashtable。存在配置时返回顶层配置键值,否则返回空 hashtable。
284+
#>
285+
[CmdletBinding()]
286+
param(
287+
[Parameter(Mandatory = $true)]
288+
[string]$ConfigPath
289+
)
290+
291+
$resolvedPath = if ([System.IO.Path]::IsPathRooted($ConfigPath)) {
292+
$ConfigPath
293+
}
294+
else {
295+
Join-Path (Get-Location).Path $ConfigPath
296+
}
297+
298+
if (-not (Test-Path -LiteralPath $resolvedPath -PathType Leaf)) {
299+
return @{}
300+
}
301+
302+
return Read-RcloneOpsConfigValues -ConfigPath $ConfigPath
303+
}
304+
305+
function Get-RcloneOpsNestedConfigValue {
306+
<#
307+
.SYNOPSIS
308+
读取嵌套 JSON 配置值。
309+
310+
.PARAMETER ConfigValues
311+
顶层 JSON 配置键值。
312+
313+
.PARAMETER Section
314+
顶层 section 名称,例如 `webui`。
315+
316+
.PARAMETER Name
317+
section 内部键名。
318+
319+
.OUTPUTS
320+
object。命中的嵌套配置值;未命中时返回 $null。
321+
#>
322+
[CmdletBinding()]
323+
param(
324+
[Parameter(Mandatory = $true)]
325+
[hashtable]$ConfigValues,
326+
327+
[Parameter(Mandatory = $true)]
328+
[string]$Section,
329+
330+
[Parameter(Mandatory = $true)]
331+
[string]$Name
332+
)
333+
334+
$sectionValue = Get-RcloneOpsConfigValue -Values $ConfigValues -Name $Section
335+
if ($null -eq $sectionValue) {
336+
return $null
337+
}
338+
339+
$sectionTable = ConvertTo-RcloneOpsHashtable -InputObject $sectionValue
340+
return Get-RcloneOpsConfigValue -Values $sectionTable -Name $Name
341+
}
342+
343+
function Get-RcloneOpsOptionWithConfig {
344+
<#
345+
.SYNOPSIS
346+
按 flag、环境变量、JSON 配置、默认值的优先级解析选项。
347+
348+
.PARAMETER Flags
349+
Split-RcloneOpsArguments 返回的 Flags 哈希表。
350+
351+
.PARAMETER Name
352+
命令行 flag 名称,不包含前缀 `--`。
353+
354+
.PARAMETER EnvName
355+
环境变量名称。
356+
357+
.PARAMETER ConfigValues
358+
顶层 JSON 配置键值。
359+
360+
.PARAMETER Section
361+
JSON section 名称。
362+
363+
.PARAMETER ConfigName
364+
JSON section 内部键名。
365+
366+
.PARAMETER DefaultValue
367+
未提供 flag、环境变量与 JSON 配置时使用的默认值。
368+
369+
.OUTPUTS
370+
System.String。解析后的选项值。
371+
#>
372+
[CmdletBinding()]
373+
param(
374+
[Parameter(Mandatory = $true)]
375+
[hashtable]$Flags,
376+
377+
[Parameter(Mandatory = $true)]
378+
[string]$Name,
379+
380+
[Parameter(Mandatory = $true)]
381+
[string]$EnvName,
382+
383+
[Parameter(Mandatory = $true)]
384+
[hashtable]$ConfigValues,
385+
386+
[Parameter(Mandatory = $true)]
387+
[string]$Section,
388+
389+
[Parameter(Mandatory = $true)]
390+
[string]$ConfigName,
391+
392+
[Parameter(Mandatory = $true)]
393+
[AllowEmptyString()]
394+
[string]$DefaultValue
395+
)
396+
397+
if ($Flags.ContainsKey($Name) -and $Flags[$Name] -is [string] -and -not [string]::IsNullOrWhiteSpace($Flags[$Name])) {
398+
return [string]$Flags[$Name]
399+
}
400+
401+
$envValue = [Environment]::GetEnvironmentVariable($EnvName, 'Process')
402+
if (-not [string]::IsNullOrWhiteSpace($envValue)) {
403+
return $envValue
404+
}
405+
406+
$configValue = Get-RcloneOpsNestedConfigValue -ConfigValues $ConfigValues -Section $Section -Name $ConfigName
407+
if ($null -ne $configValue -and -not [string]::IsNullOrWhiteSpace([string]$configValue)) {
408+
return [string](Resolve-RcloneOpsEnvPlaceholder -Value $configValue -Context "$Section.$ConfigName")
409+
}
410+
411+
return $DefaultValue
412+
}
413+
274414
function Resolve-RcloneOpsEnvPlaceholder {
275415
<#
276416
.SYNOPSIS
@@ -619,11 +759,13 @@ function Start-RcloneOpsWebUi {
619759
[string[]]$Passthrough
620760
)
621761

762+
$sourcePath = Get-RcloneOpsOption -Flags $Flags -Name 'source' -EnvName 'RCLONE_SOURCE_CONFIG_PATH' -DefaultValue $script:DefaultSourcePath
763+
$sourceValues = Get-RcloneOpsOptionalConfigValues -ConfigPath $sourcePath
622764
$rclone = Get-RcloneOpsOption -Flags $Flags -Name 'rclone' -EnvName 'RCLONE_BIN' -DefaultValue 'rclone'
623765
$configPath = Get-RcloneOpsOption -Flags $Flags -Name 'config' -EnvName 'RCLONE_CONFIG_PATH' -DefaultValue $script:DefaultConfigPath
624-
$rcAddr = Get-RcloneOpsOption -Flags $Flags -Name 'addr' -EnvName 'RCLONE_RC_ADDR' -DefaultValue $script:DefaultRcAddr
625-
$rcPass = Get-RcloneOpsOption -Flags $Flags -Name 'pass' -EnvName 'RCLONE_RC_PASS' -DefaultValue ''
626-
$rcUser = if ($rcPass) { Get-RcloneOpsOption -Flags $Flags -Name 'user' -EnvName 'RCLONE_RC_USER' -DefaultValue $script:DefaultRcUser } else { '' }
766+
$rcAddr = Get-RcloneOpsOptionWithConfig -Flags $Flags -Name 'addr' -EnvName 'RCLONE_RC_ADDR' -ConfigValues $sourceValues -Section 'webui' -ConfigName 'addr' -DefaultValue $script:DefaultRcAddr
767+
$rcPass = Get-RcloneOpsOptionWithConfig -Flags $Flags -Name 'pass' -EnvName 'RCLONE_RC_PASS' -ConfigValues $sourceValues -Section 'webui' -ConfigName 'pass' -DefaultValue ''
768+
$rcUser = if ($rcPass) { Get-RcloneOpsOptionWithConfig -Flags $Flags -Name 'user' -EnvName 'RCLONE_RC_USER' -ConfigValues $sourceValues -Section 'webui' -ConfigName 'user' -DefaultValue $script:DefaultRcUser } else { '' }
627769
$isBackground = $Flags.ContainsKey('background')
628770
$logFile = Get-RcloneOpsOption -Flags $Flags -Name 'log-file' -EnvName 'RCLONE_LOG_FILE' -DefaultValue (Join-Path $script:DefaultLogDir 'webui.log')
629771
if ($isBackground) {

config/service/oss/rclone/rclone-ops.test.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
parseArgs,
77
renderRcloneConfig,
88
resolveEnvPlaceholders,
9+
resolveOptionWithConfig,
910
} from './rclone-ops.mjs'
1011

1112
describe('rclone-ops JSON 配置生成逻辑', () => {
@@ -80,6 +81,24 @@ describe('rclone-ops JSON 配置生成逻辑', () => {
8081
assert.throws(() => buildRemoteDefinitions({}), / remotes /)
8182
})
8283

84+
85+
it('能从 JSON webui section 读取 RC 密码', () => {
86+
process.env.RCLONE_RC_PASS = 'json-rc-pass'
87+
88+
assert.equal(
89+
resolveOptionWithConfig(
90+
new Map(),
91+
'pass',
92+
'UNUSED_RCLONE_RC_PASS',
93+
{ webui: { pass: '${RCLONE_RC_PASS}' } },
94+
'webui',
95+
'pass',
96+
'',
97+
),
98+
'json-rc-pass',
99+
)
100+
})
101+
83102
it('能渲染并重新读取 remote 名称', () => {
84103
const config = renderRcloneConfig([
85104
{ name: 'cloud-main', type: 's3', provider: 'Other' },

config/service/oss/rclone/rclone.config.example.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@
2121
"force_path_style": "true",
2222
"no_check_bucket": "true"
2323
}
24-
]
24+
],
25+
"webui": {
26+
"addr": "127.0.0.1:5572",
27+
"user": "admin",
28+
"pass": "${RCLONE_RC_PASS}"
29+
}
2530
}

tests/RcloneOps.Tests.ps1

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ AfterAll {
3636
'Get-RcloneOpsRemoteName',
3737
'Read-RcloneOpsConfigValues',
3838
'Resolve-RcloneOpsEnvPlaceholder',
39-
'Split-RcloneOpsArguments'
39+
'Split-RcloneOpsArguments',
40+
'Get-RcloneOpsOptionWithConfig'
4041
)) {
4142
Remove-Item -Path ("Function:\{0}" -f $functionName) -ErrorAction SilentlyContinue
4243
}
@@ -134,6 +135,22 @@ Describe 'rclone-ops.ps1 JSON 配置生成逻辑' {
134135
{ Read-RcloneOpsConfigValues -ConfigPath $envPath } | Should -Throw 'rclone-ops 仅支持 JSON 主配置*'
135136
}
136137

138+
139+
It '能从 JSON webui section 读取 RC 密码' {
140+
$env:RCLONE_RC_PASS = 'json-rc-pass'
141+
$values = @{
142+
webui = [pscustomobject]@{
143+
addr = '100.64.0.1:5572'
144+
user = 'admin'
145+
pass = '${RCLONE_RC_PASS}'
146+
}
147+
}
148+
149+
$password = Get-RcloneOpsOptionWithConfig -Flags @{} -Name 'pass' -EnvName 'UNUSED_RCLONE_RC_PASS' -ConfigValues $values -Section 'webui' -ConfigName 'pass' -DefaultValue ''
150+
151+
$password | Should -Be 'json-rc-pass'
152+
}
153+
137154
It '能解析透传参数与布尔开关' {
138155
$parsed = Split-RcloneOpsArguments -ArgumentList @('source', 'dest', '--run', '--', '--progress')
139156

0 commit comments

Comments
 (0)