remote_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package main
  2. import (
  3. "errors"
  4. "os"
  5. "os/exec"
  6. "path/filepath"
  7. "strings"
  8. "testing"
  9. "time"
  10. )
  11. func TestBuildRemote(t *testing.T) {
  12. env, err := buildRemote(remoteSpec{Type: "s3", Params: map[string]string{"provider": "Minio", "endpoint": "http://127.0.0.1:9198", "bucket": "kb", "prefix": "/docs/2024/"}, Secrets: map[string]string{"accessKeyId": "ak", "secretAccessKey": "sk"}}, "")
  13. if err != nil {
  14. t.Fatal(err)
  15. }
  16. if env.path != "kb:kb/docs/2024" {
  17. t.Errorf("远程路径:%s", env.path)
  18. }
  19. joined := strings.Join(env.env, "\n")
  20. for _, want := range []string{"RCLONE_CONFIG_KB_TYPE=s3", "RCLONE_CONFIG_KB_PROVIDER=Minio", "RCLONE_CONFIG_KB_ENDPOINT=http://127.0.0.1:9198", "RCLONE_CONFIG_KB_SECRET_ACCESS_KEY=sk", "RCLONE_CONFIG_KB_ENV_AUTH=false"} {
  21. if !strings.Contains(joined, want) {
  22. t.Errorf("缺少 %s", want)
  23. }
  24. }
  25. bad := []remoteSpec{
  26. {Type: "s3", Params: map[string]string{"endpoint": "http://x", "bucket": "kb"}, Secrets: map[string]string{"accessKeyId": "ak"}},
  27. {Type: "s3", Params: map[string]string{"endpoint": "ftp://x", "bucket": "kb"}, Secrets: map[string]string{"accessKeyId": "a", "secretAccessKey": "b"}},
  28. {Type: "s3", Params: map[string]string{"endpoint": "http://x", "bucket": "-rf"}, Secrets: map[string]string{"accessKeyId": "a", "secretAccessKey": "b"}},
  29. {Type: "s3", Params: map[string]string{"endpoint": "http://x", "bucket": "kb", "prefix": "a/../../b"}, Secrets: map[string]string{"accessKeyId": "a", "secretAccessKey": "b"}},
  30. {Type: "s3", Params: map[string]string{"endpoint": "http://x\nRCLONE_X=1", "bucket": "kb"}, Secrets: map[string]string{"accessKeyId": "a", "secretAccessKey": "b"}},
  31. {Type: "webdav", Params: map[string]string{"url": "http://u:p@dav.local/"}},
  32. {Type: "webdav", Params: map[string]string{"url": "http://dav.local/", "vendor": "evil"}},
  33. {Type: "local", Params: map[string]string{"path": "/"}},
  34. {Type: "sftp"},
  35. }
  36. for i, r := range bad {
  37. if _, err := buildRemote(r, ""); err == nil {
  38. t.Errorf("第 %d 个配置应该被拒绝:%+v", i, r)
  39. }
  40. }
  41. }
  42. func TestClassify(t *testing.T) {
  43. cases := map[string]string{
  44. "ERROR : api error SignatureDoesNotMatch: The request signature we calculated does not match": "auth_failed",
  45. "ERROR : https response error StatusCode: 403, api error AccessDenied: Access Denied.": "auth_failed",
  46. "ERROR : api error NoSuchBucket: The specified bucket does not exist": "path_missing",
  47. `ERROR : request send failed, Get "http://127.0.0.1:1/x": dial tcp 127.0.0.1:1: connect: connection refused`: "unreachable",
  48. "ERROR : dial tcp: lookup nas.local: no such host": "unreachable",
  49. }
  50. for in, want := range cases {
  51. if st, msg := classify(in); st != want || msg == "" {
  52. t.Errorf("%q → %s %q,应为 %s", in, st, msg, want)
  53. }
  54. }
  55. if _, msg := classify("2026/11/06 ERROR : api error AccessDenied: Access Denied."); msg != "认证失败或没有权限:AccessDenied:Access Denied." {
  56. t.Errorf("错误信息:%q", msg)
  57. }
  58. long := `ERROR : error listing: operation error S3: ListObjectsV2, exceeded maximum number of attempts, 1, request send failed, Get "http://127.0.0.1:1/kb?x=1": dial tcp 127.0.0.1:1: connect: connection refused`
  59. if _, msg := classify(long); msg != "无法连接:dial tcp 127.0.0.1:1: connect: connection refused" {
  60. t.Errorf("网络错误信息:%q", msg)
  61. }
  62. }
  63. // 需要 rclone:RCLONE_BIN 或 PATH 中的 rclone;用 rclone 的 local 后端,不需要网络
  64. func rcloneBin(t *testing.T) string {
  65. if b := os.Getenv("RCLONE_BIN"); b != "" {
  66. return b
  67. }
  68. if b, err := exec.LookPath("rclone"); err == nil {
  69. return b
  70. }
  71. t.Skip("没有 rclone(设置 RCLONE_BIN)")
  72. return ""
  73. }
  74. func TestRemoteMount(t *testing.T) {
  75. needFuse(t)
  76. bin := rcloneBin(t)
  77. allowLocalRemote = true
  78. defer func() { allowLocalRemote = false }()
  79. base, roots := tempRoots(t)
  80. data := filepath.Join(base, "remote-data")
  81. os.MkdirAll(filepath.Join(data, "产品手册", ".git"), 0o755)
  82. os.WriteFile(filepath.Join(data, "产品手册", "规格.csv"), []byte("型号,功率\nA100,5\n"), 0o644)
  83. os.WriteFile(filepath.Join(data, ".env"), []byte("SECRET=1"), 0o644)
  84. dir := filepath.Join(base, "mounter")
  85. mntDir := filepath.Join(base, "kbmnt")
  86. for _, d := range []string{dir, mntDir, filepath.Join(dir, "remote"), filepath.Join(dir, "cache")} {
  87. os.MkdirAll(d, 0o755)
  88. }
  89. conf := filepath.Join(dir, "rclone.conf")
  90. os.WriteFile(conf, nil, 0o600)
  91. m := newManager(realSystem{}, roots, mntDir, filepath.Join(dir, "state.json")).withRemote(&rcloneRunner{bin: bin, config: conf, home: dir}, filepath.Join(dir, "remote"), filepath.Join(dir, "cache"))
  92. defer m.unmountAll()
  93. spec := mountSpec{ID: "kb_r1", Kind: "remote", Remote: &remoteSpec{Type: "local", Params: map[string]string{"path": data}}}
  94. st, err := m.sync([]mountSpec{spec}, true)
  95. if err != nil || len(st) != 1 || st[0].State != "ok" {
  96. t.Fatalf("远程存储挂载:%+v %v", st, err)
  97. }
  98. mp := filepath.Join(mntDir, "kb_r1")
  99. if got := names(t, mp); len(got) != 1 || got[0] != "产品手册" {
  100. t.Errorf("目录列表(隐藏文件不可见):%v", got)
  101. }
  102. if b, err := os.ReadFile(filepath.Join(mp, "产品手册", "规格.csv")); err != nil || !strings.Contains(string(b), "A100") {
  103. t.Errorf("读取:%q %v", b, err)
  104. }
  105. if _, err := os.Stat(filepath.Join(mp, ".env")); !errors.Is(err, os.ErrNotExist) {
  106. t.Errorf(".env 应该不存在:%v", err)
  107. }
  108. if err := os.WriteFile(filepath.Join(mp, "x.txt"), []byte("x"), 0o644); err == nil {
  109. t.Error("应该不能写入")
  110. }
  111. // 凭据不出现在命令行
  112. m.mu.Lock()
  113. cmd := m.mounts["kb_r1"].remote.cmd
  114. m.mu.Unlock()
  115. if strings.Contains(strings.Join(cmd.Args, " "), "RCLONE_CONFIG") {
  116. t.Errorf("命令行中不应有配置:%v", cmd.Args)
  117. }
  118. // 配置错误:状态为挂载失败
  119. bad := mountSpec{ID: "kb_r2", Kind: "remote", Remote: &remoteSpec{Type: "s3", Params: map[string]string{"bucket": "x"}}}
  120. st, _ = m.sync([]mountSpec{spec, bad}, false)
  121. if st[1].State != "mount_failed" {
  122. t.Errorf("配置错误:%+v", st[1])
  123. }
  124. // 删除:停止 rclone、卸载、清理缓存
  125. if _, err := m.sync(nil, false); err != nil {
  126. t.Fatal(err)
  127. }
  128. time.Sleep(200 * time.Millisecond)
  129. for _, p := range []string{mp, filepath.Join(dir, "remote", "kb_r1"), filepath.Join(dir, "cache", "kb_r1")} {
  130. if _, err := os.Stat(p); !errors.Is(err, os.ErrNotExist) {
  131. t.Errorf("%s 应该已删除:%v", p, err)
  132. }
  133. }
  134. if cmd.ProcessState == nil {
  135. t.Error("rclone 应该已退出")
  136. }
  137. }