remote.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. package main
  2. // 远程存储(docs/design/21-knowledge-sources.md §6):每个资料库一个 rclone 子进程,只读挂到 mounter/remote/<id>(内部,只有本用户能访问),
  3. // 再由本进程的文件系统(隐藏文件过滤、只读)提供给 knowledge/mnt/<id>。
  4. // 连接参数和凭据通过子进程的环境变量传给 rclone,不放在命令行里(命令行在进程列表中所有人可见)。
  5. import (
  6. "bytes"
  7. "context"
  8. "fmt"
  9. "log"
  10. "net/url"
  11. "os"
  12. "os/exec"
  13. "path/filepath"
  14. "regexp"
  15. "sort"
  16. "strings"
  17. "sync"
  18. "syscall"
  19. "time"
  20. )
  21. type remoteSpec struct {
  22. Type string `json:"type"`
  23. Params map[string]string `json:"params"`
  24. Secrets map[string]string `json:"secrets"`
  25. }
  26. // allowLocalRemote 仅测试使用:rclone 的 local 后端,不需要网络
  27. var allowLocalRemote = false
  28. var (
  29. bucketPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$`)
  30. s3Providers = map[string]bool{"Minio": true, "AWS": true, "Alibaba": true, "TencentCOS": true, "HuaweiOBS": true, "Qiniu": true, "Ceph": true, "Cloudflare": true, "Other": true}
  31. webdavVendors = map[string]bool{"other": true, "nextcloud": true, "owncloud": true, "sharepoint": true, "sharepoint-ntlm": true}
  32. )
  33. type rcloneEnv struct {
  34. env []string // RCLONE_CONFIG_KB_*
  35. path string // kb:bucket/prefix
  36. }
  37. func clean(v string) (string, error) {
  38. v = strings.TrimSpace(v)
  39. if strings.ContainsAny(v, "\x00\r\n") {
  40. return "", fmt.Errorf("参数中不能有换行")
  41. }
  42. return v, nil
  43. }
  44. func httpURL(v, name string) (string, error) {
  45. u, err := url.Parse(v)
  46. if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
  47. return "", fmt.Errorf("%s必须是 http 或 https 地址", name)
  48. }
  49. if u.User != nil {
  50. return "", fmt.Errorf("%s中不能包含用户名或密码", name)
  51. }
  52. return v, nil
  53. }
  54. // buildRemote 按存储类型生成 rclone 的环境变量与远程路径
  55. func buildRemote(r remoteSpec, rclone string) (*rcloneEnv, error) {
  56. p := map[string]string{}
  57. for k, v := range r.Params {
  58. c, err := clean(v)
  59. if err != nil {
  60. return nil, err
  61. }
  62. p[k] = c
  63. }
  64. sec := func(k string) (string, error) {
  65. v := r.Secrets[k]
  66. if strings.ContainsAny(v, "\x00\r\n") {
  67. return "", fmt.Errorf("凭据中不能有换行")
  68. }
  69. return v, nil
  70. }
  71. out := &rcloneEnv{}
  72. add := func(k, v string) { out.env = append(out.env, "RCLONE_CONFIG_KB_"+k+"="+v) }
  73. switch r.Type {
  74. case "s3":
  75. provider := p["provider"]
  76. if provider == "" {
  77. provider = "Other"
  78. }
  79. if !s3Providers[provider] {
  80. return nil, fmt.Errorf("不支持的 S3 服务商:%s", provider)
  81. }
  82. if p["endpoint"] == "" && provider != "AWS" {
  83. return nil, fmt.Errorf("请填写服务地址(endpoint)")
  84. }
  85. if p["endpoint"] != "" {
  86. if _, err := httpURL(p["endpoint"], "服务地址"); err != nil {
  87. return nil, err
  88. }
  89. }
  90. if !bucketPattern.MatchString(p["bucket"]) {
  91. return nil, fmt.Errorf("存储桶名称不正确")
  92. }
  93. ak, err := sec("accessKeyId")
  94. if err != nil {
  95. return nil, err
  96. }
  97. sk, err := sec("secretAccessKey")
  98. if err != nil {
  99. return nil, err
  100. }
  101. if ak == "" || sk == "" {
  102. return nil, fmt.Errorf("请填写 Access Key 和 Secret Key")
  103. }
  104. add("TYPE", "s3")
  105. add("PROVIDER", provider)
  106. add("ENV_AUTH", "false")
  107. add("ACCESS_KEY_ID", ak)
  108. add("SECRET_ACCESS_KEY", sk)
  109. add("NO_CHECK_BUCKET", "true")
  110. if p["endpoint"] != "" {
  111. add("ENDPOINT", p["endpoint"])
  112. }
  113. if p["region"] != "" {
  114. add("REGION", p["region"])
  115. }
  116. prefix := strings.Trim(p["prefix"], "/")
  117. if strings.Contains("/"+prefix+"/", "/../") {
  118. return nil, fmt.Errorf("路径前缀不能包含 ..")
  119. }
  120. out.path = "kb:" + p["bucket"]
  121. if prefix != "" {
  122. out.path += "/" + prefix
  123. }
  124. case "webdav":
  125. u, err := httpURL(p["url"], "地址")
  126. if err != nil {
  127. return nil, err
  128. }
  129. vendor := p["vendor"]
  130. if vendor == "" {
  131. vendor = "other"
  132. }
  133. if !webdavVendors[vendor] {
  134. return nil, fmt.Errorf("不支持的 WebDAV 服务类型:%s", vendor)
  135. }
  136. add("TYPE", "webdav")
  137. add("URL", u)
  138. add("VENDOR", vendor)
  139. if p["user"] != "" {
  140. add("USER", p["user"])
  141. }
  142. pass, err := sec("password")
  143. if err != nil {
  144. return nil, err
  145. }
  146. if pass != "" {
  147. ob, err := obscure(rclone, pass)
  148. if err != nil {
  149. return nil, err
  150. }
  151. add("PASS", ob)
  152. }
  153. out.path = "kb:"
  154. case "local":
  155. if !allowLocalRemote {
  156. return nil, fmt.Errorf("不支持的存储类型:local")
  157. }
  158. add("TYPE", "local")
  159. out.path = "kb:" + p["path"]
  160. default:
  161. return nil, fmt.Errorf("不支持的存储类型:%s", r.Type)
  162. }
  163. return out, nil
  164. }
  165. // obscure rclone 的 WebDAV 密码需要先混淆;明文从标准输入传入,不出现在命令行
  166. func obscure(rclone, pass string) (string, error) {
  167. cmd := exec.Command(rclone, "obscure", "-")
  168. cmd.Env = []string{"PATH=/usr/bin:/bin"}
  169. cmd.Stdin = strings.NewReader(pass)
  170. out, err := cmd.Output()
  171. if err != nil {
  172. return "", fmt.Errorf("处理密码失败:%v", err)
  173. }
  174. return strings.TrimSpace(string(out)), nil
  175. }
  176. // classify 根据 rclone 的错误输出判断状态
  177. func classify(stderr string) (state, msg string) {
  178. low := strings.ToLower(stderr)
  179. detail := lastError(stderr)
  180. switch {
  181. case containsAny(low, "signaturedoesnotmatch", "invalidaccesskeyid", "accessdenied", "access denied", "statuscode: 401", "statuscode: 403", "401 unauthorized", "403 forbidden", "authentication", "unauthorized"):
  182. return "auth_failed", "认证失败或没有权限:" + detail
  183. case containsAny(low, "nosuchbucket", "bucket does not exist", "directory not found", "404 not found", "statuscode: 404"):
  184. return "path_missing", "存储桶或路径不存在:" + detail
  185. case containsAny(low, "connection refused", "no such host", "i/o timeout", "timeout", "network is unreachable", "tls:", "x509", "connection reset", "unexpected eof"):
  186. return "unreachable", "无法连接:" + detail
  187. }
  188. if detail == "" {
  189. detail = "未知错误"
  190. }
  191. return "unreachable", detail
  192. }
  193. var (
  194. apiErr = regexp.MustCompile(`api error ([A-Za-z]+): ([^\n]+)`)
  195. netErr = regexp.MustCompile(`(dial tcp [^\n]*|lookup [^\n]*|tls: [^\n]*|x509: [^\n]*)`)
  196. )
  197. func lastError(s string) string {
  198. if m := apiErr.FindAllStringSubmatch(s, -1); len(m) > 0 {
  199. last := m[len(m)-1]
  200. return strings.TrimSpace(last[1] + ":" + last[2])
  201. }
  202. if m := netErr.FindAllString(s, -1); len(m) > 0 {
  203. return strings.TrimSpace(m[len(m)-1])
  204. }
  205. lines := strings.Split(strings.TrimSpace(s), "\n")
  206. for i := len(lines) - 1; i >= 0; i-- {
  207. l := lines[i]
  208. if strings.Contains(l, "ERROR") || strings.Contains(l, "CRITICAL") || strings.Contains(l, "Failed") {
  209. if j := strings.Index(l, ": "); j >= 0 && strings.Contains(l[:j], "/") {
  210. l = l[j+2:]
  211. }
  212. if len(l) > 300 {
  213. l = l[:300] + "…"
  214. }
  215. return strings.TrimSpace(l)
  216. }
  217. }
  218. return ""
  219. }
  220. func containsAny(s string, subs ...string) bool {
  221. for _, x := range subs {
  222. if strings.Contains(s, x) {
  223. return true
  224. }
  225. }
  226. return false
  227. }
  228. // rcloneRunner 运行 rclone 的公共参数
  229. type rcloneRunner struct {
  230. bin string
  231. config string // 空的配置文件,避免读取部署用户自己的 rclone.conf
  232. home string
  233. }
  234. func (r *rcloneRunner) command(ctx context.Context, env *rcloneEnv, args ...string) *exec.Cmd {
  235. cmd := exec.CommandContext(ctx, r.bin, append(args, "--config", r.config)...)
  236. cmd.Env = append([]string{"PATH=/usr/bin:/bin", "HOME=" + r.home}, env.env...)
  237. return cmd
  238. }
  239. type probeResult struct {
  240. State string `json:"state"`
  241. Message string `json:"message"`
  242. Entries []string `json:"entries"`
  243. }
  244. // probe 列出远程根目录(带超时),用于"测试连接"与状态检查
  245. func (r *rcloneRunner) probe(parent context.Context, env *rcloneEnv, timeout time.Duration) probeResult {
  246. ctx, cancel := context.WithTimeout(parent, timeout)
  247. defer cancel()
  248. cmd := r.command(ctx, env, "lsf", env.path, "--max-depth", "1", "--low-level-retries", "1", "--retries", "1", "--contimeout", "5s", "--timeout", "10s", "--log-level", "ERROR")
  249. var out, errb bytes.Buffer
  250. cmd.Stdout, cmd.Stderr = &limitWriter{w: &out, n: 64 << 10}, &limitWriter{w: &errb, n: 64 << 10}
  251. err := cmd.Run()
  252. if ctx.Err() == context.DeadlineExceeded {
  253. return probeResult{State: "unreachable", Message: "连接超时"}
  254. }
  255. if ctx.Err() != nil {
  256. return probeResult{State: "pending", Message: "已取消"}
  257. }
  258. // rclone lsf 出错时退出码可能为 0,以错误输出为准
  259. if err != nil || strings.Contains(errb.String(), "ERROR") {
  260. st, msg := classify(errb.String())
  261. return probeResult{State: st, Message: msg}
  262. }
  263. var entries []string
  264. for _, l := range strings.Split(strings.TrimSpace(out.String()), "\n") {
  265. if l != "" && !strings.HasPrefix(l, ".") && len(entries) < 50 {
  266. entries = append(entries, l)
  267. }
  268. }
  269. return probeResult{State: "ok", Entries: entries}
  270. }
  271. type limitWriter struct {
  272. w *bytes.Buffer
  273. n int
  274. }
  275. func (l *limitWriter) Write(p []byte) (int, error) {
  276. if rest := l.n - l.w.Len(); rest > 0 {
  277. if len(p) > rest {
  278. l.w.Write(p[:rest])
  279. } else {
  280. l.w.Write(p)
  281. }
  282. }
  283. return len(p), nil
  284. }
  285. // ringLog 保留 rclone 最近的错误输出
  286. type ringLog struct {
  287. mu sync.Mutex
  288. buf []string
  289. }
  290. func (r *ringLog) Write(p []byte) (int, error) {
  291. r.mu.Lock()
  292. defer r.mu.Unlock()
  293. for _, l := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") {
  294. if l == "" {
  295. continue
  296. }
  297. r.buf = append(r.buf, l)
  298. if len(r.buf) > 50 {
  299. r.buf = r.buf[len(r.buf)-50:]
  300. }
  301. }
  302. return len(p), nil
  303. }
  304. func (r *ringLog) String() string {
  305. r.mu.Lock()
  306. defer r.mu.Unlock()
  307. return strings.Join(r.buf, "\n")
  308. }
  309. func (r *ringLog) reset() {
  310. r.mu.Lock()
  311. r.buf = nil
  312. r.mu.Unlock()
  313. }
  314. // remoteProc 一个远程存储的 rclone 挂载及其状态
  315. type remoteProc struct {
  316. id string
  317. point string // 内部挂载点
  318. cache string
  319. runner *rcloneRunner
  320. sys system
  321. env *rcloneEnv
  322. key string // 配置的指纹,变化时重启
  323. mu sync.Mutex
  324. mounted bool
  325. state string
  326. message string
  327. probedAt time.Time
  328. ready chan struct{} // 首次得出状态后关闭
  329. readyOnce sync.Once
  330. stop chan struct{}
  331. done chan struct{}
  332. cmd *exec.Cmd
  333. logs ringLog
  334. onChange func()
  335. }
  336. func (p *remoteProc) status() (mounted bool, state, msg string) {
  337. p.mu.Lock()
  338. defer p.mu.Unlock()
  339. return p.mounted, p.state, p.message
  340. }
  341. func (p *remoteProc) set(mounted bool, state, msg string) {
  342. p.mu.Lock()
  343. changed := p.mounted != mounted || p.state != state || p.message != msg
  344. p.mounted, p.state, p.message = mounted, state, msg
  345. p.mu.Unlock()
  346. if state != "pending" {
  347. p.readyOnce.Do(func() { close(p.ready) })
  348. }
  349. if changed && p.onChange != nil {
  350. p.onChange()
  351. }
  352. }
  353. // isMounted 内部挂载点上确实是 rclone 的挂载(而不是下面的空目录)
  354. func (p *remoteProc) isMounted() bool {
  355. for _, e := range p.sys.mountinfo() {
  356. if e.Point == p.point && strings.HasPrefix(e.FSType, "fuse.rclone") {
  357. return true
  358. }
  359. }
  360. return false
  361. }
  362. // run 启动并看护 rclone:退出时按退避间隔重启
  363. func (p *remoteProc) run() {
  364. defer close(p.done)
  365. backoff := 5 * time.Second
  366. for {
  367. started := time.Now()
  368. p.runOnce()
  369. select {
  370. case <-p.stop:
  371. return
  372. default:
  373. }
  374. if time.Since(started) > time.Minute {
  375. backoff = 5 * time.Second
  376. }
  377. select {
  378. case <-p.stop:
  379. return
  380. case <-time.After(backoff):
  381. }
  382. if backoff < 5*time.Minute {
  383. backoff *= 2
  384. }
  385. }
  386. }
  387. func (p *remoteProc) runOnce() {
  388. _ = exec.Command("fusermount3", "-uz", p.point).Run()
  389. if err := os.MkdirAll(p.point, 0o700); err != nil {
  390. p.set(false, "mount_failed", "创建内部挂载点失败:"+err.Error())
  391. return
  392. }
  393. _ = os.MkdirAll(p.cache, 0o700)
  394. p.logs.reset()
  395. cmd := p.runner.command(context.Background(), p.env, "mount", p.env.path, p.point,
  396. "--read-only", "--vfs-cache-mode", "full", "--cache-dir", p.cache,
  397. "--vfs-cache-max-size", "1G", "--vfs-cache-max-age", "1h", "--dir-cache-time", "1m",
  398. "--low-level-retries", "3", "--contimeout", "10s", "--timeout", "60s", "--log-level", "NOTICE")
  399. cmd.Stdout, cmd.Stderr = &p.logs, &p.logs
  400. cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM}
  401. if err := cmd.Start(); err != nil {
  402. p.set(false, "mount_failed", "启动 rclone 失败:"+err.Error())
  403. return
  404. }
  405. p.mu.Lock()
  406. p.cmd = cmd
  407. p.mu.Unlock()
  408. exited := make(chan error, 1)
  409. go func() { exited <- cmd.Wait() }()
  410. // 等挂载出现
  411. deadline := time.After(30 * time.Second)
  412. tick := time.NewTicker(200 * time.Millisecond)
  413. defer tick.Stop()
  414. wait:
  415. for {
  416. select {
  417. case err := <-exited:
  418. st, msg := classify(p.logs.String())
  419. if msg == "未知错误" || msg == "" {
  420. msg = fmt.Sprintf("rclone 退出:%v", err)
  421. }
  422. log.Printf("%s:rclone 退出:%s", p.id, lastError(p.logs.String()))
  423. p.set(false, st, msg)
  424. return
  425. case <-p.stop:
  426. p.terminate(exited)
  427. return
  428. case <-deadline:
  429. log.Printf("%s:rclone 30 秒内没有完成挂载", p.id)
  430. p.set(false, "unreachable", "挂载超时")
  431. p.terminate(exited)
  432. return
  433. case <-tick.C:
  434. if p.isMounted() {
  435. break wait
  436. }
  437. }
  438. }
  439. log.Printf("%s:rclone 已挂载 %s", p.id, p.env.path)
  440. p.set(true, "pending", "正在连接")
  441. p.checkNow()
  442. // 运行期间:未连通时每分钟检查一次,正常时每 5 分钟
  443. for {
  444. _, state, _ := p.status()
  445. every := 5 * time.Minute
  446. if state != "ok" {
  447. every = time.Minute
  448. }
  449. select {
  450. case err := <-exited:
  451. st, msg := "unreachable", fmt.Sprintf("rclone 意外退出(%v),稍后自动重启", err)
  452. if detail := lastError(p.logs.String()); detail != "" {
  453. st, msg = classify(p.logs.String())
  454. }
  455. log.Printf("%s:rclone 意外退出(%v):%s", p.id, err, lastError(p.logs.String()))
  456. p.set(false, st, msg)
  457. _ = exec.Command("fusermount3", "-uz", p.point).Run()
  458. return
  459. case <-p.stop:
  460. p.terminate(exited)
  461. return
  462. case <-time.After(every):
  463. p.checkNow()
  464. }
  465. }
  466. }
  467. // checkNow 列远程根目录,得出连接状态
  468. func (p *remoteProc) checkNow() {
  469. ctx, cancel := context.WithCancel(context.Background())
  470. defer cancel()
  471. go func() {
  472. select {
  473. case <-p.stop:
  474. cancel()
  475. case <-ctx.Done():
  476. }
  477. }()
  478. r := p.runner.probe(ctx, p.env, 20*time.Second)
  479. if r.State == "pending" {
  480. return
  481. }
  482. if r.State == "ok" {
  483. // 之前的错误已经过去,rclone 之后退出时不再据此判断
  484. p.logs.reset()
  485. }
  486. p.mu.Lock()
  487. p.probedAt = time.Now()
  488. mounted := p.mounted
  489. p.mu.Unlock()
  490. p.set(mounted, r.State, r.Message)
  491. }
  492. // checkIfStale 状态查询时:超过 30 秒没检查过的,在后台重新检查
  493. func (p *remoteProc) checkIfStale() {
  494. p.mu.Lock()
  495. stale := p.mounted && time.Since(p.probedAt) > 30*time.Second
  496. if stale {
  497. p.probedAt = time.Now()
  498. }
  499. p.mu.Unlock()
  500. if stale {
  501. go p.checkNow()
  502. }
  503. }
  504. func (p *remoteProc) terminate(exited chan error) {
  505. p.mu.Lock()
  506. cmd := p.cmd
  507. p.mu.Unlock()
  508. if cmd != nil && cmd.Process != nil {
  509. _ = cmd.Process.Signal(syscall.SIGTERM)
  510. select {
  511. case <-exited:
  512. case <-time.After(5 * time.Second):
  513. _ = cmd.Process.Kill()
  514. <-exited
  515. }
  516. }
  517. _ = exec.Command("fusermount3", "-uz", p.point).Run()
  518. }
  519. func newRemoteProc(id, remoteDir, cacheDir string, runner *rcloneRunner, sys system, env *rcloneEnv, key string, onChange func()) *remoteProc {
  520. return &remoteProc{
  521. id: id, point: filepath.Join(remoteDir, id), cache: filepath.Join(cacheDir, id), runner: runner, sys: sys, env: env, key: key,
  522. state: "pending", message: "正在连接", ready: make(chan struct{}), stop: make(chan struct{}), done: make(chan struct{}), onChange: onChange,
  523. }
  524. }
  525. func (p *remoteProc) start() { go p.run() }
  526. // shutdown 停止 rclone、卸载、删除缓存
  527. func (p *remoteProc) shutdown() {
  528. close(p.stop)
  529. <-p.done
  530. _ = os.Remove(p.point)
  531. _ = os.RemoveAll(p.cache)
  532. }
  533. // remoteKey 配置指纹(含凭据),用于判断是否需要重启 rclone
  534. func remoteKey(r *remoteSpec) string {
  535. var b strings.Builder
  536. b.WriteString(r.Type)
  537. for _, m := range []map[string]string{r.Params, r.Secrets} {
  538. keys := make([]string, 0, len(m))
  539. for k := range m {
  540. keys = append(keys, k)
  541. }
  542. sort.Strings(keys)
  543. for _, k := range keys {
  544. fmt.Fprintf(&b, "\x00%s=%s", k, m[k])
  545. }
  546. b.WriteString("\x01")
  547. }
  548. return b.String()
  549. }