client.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /*
  2. * Rufs - Remote Union File System
  3. *
  4. * Copyright 2017 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the MIT
  7. * License, If a copy of the MIT License was not distributed with this
  8. * file, You can obtain one at https://opensource.org/licenses/MIT.
  9. */
  10. package main
  11. import (
  12. "flag"
  13. "fmt"
  14. "io/ioutil"
  15. "os"
  16. "runtime"
  17. "devt.de/krotik/common/datautil"
  18. "devt.de/krotik/common/errorutil"
  19. "devt.de/krotik/common/fileutil"
  20. "devt.de/krotik/common/termutil"
  21. "devt.de/krotik/rufs"
  22. "devt.de/krotik/rufs/config"
  23. "devt.de/krotik/rufs/term"
  24. )
  25. /*
  26. DefaultMappingFile is the default mapping file for client trees
  27. */
  28. const DefaultMappingFile = "rufs.mapping.json"
  29. /*
  30. clientCli handles the client command line.
  31. */
  32. func clientCli() error {
  33. var tree *rufs.Tree
  34. var err error
  35. var fuseMount, dokanMount, webExport *string
  36. if runtime.GOOS == "linux" {
  37. fuseMount = flag.String("fuse-mount", "", "Mount tree as FUSE filesystem at specified path (read-only)")
  38. }
  39. if runtime.GOOS == "windows" {
  40. dokanMount = flag.String("dokan-mount", "", "Mount tree as DOKAN filesystem at specified path (read-only)")
  41. }
  42. webExport = flag.String("web", "", "Export the tree through a https interface on the specified host:port")
  43. secretFile, certDir := commonCliOptions()
  44. showHelp := flag.Bool("help", false, "Show this help message")
  45. flag.Usage = func() {
  46. fmt.Println()
  47. fmt.Println(fmt.Sprintf("Usage of %s client [mapping file]", os.Args[0]))
  48. fmt.Println()
  49. flag.PrintDefaults()
  50. fmt.Println()
  51. fmt.Println("The mapping file assignes remote branches to the local tree.")
  52. fmt.Println(fmt.Sprintf("The client tries to load %v if no mapping file is defined.",
  53. DefaultMappingFile))
  54. fmt.Println("It starts empty if no mapping file exists. The mapping file")
  55. fmt.Println("should have the following json format:")
  56. fmt.Println()
  57. fmt.Println("{")
  58. fmt.Println(` "branches" : [`)
  59. fmt.Println(` {`)
  60. fmt.Println(` "branch" : <branch name>,`)
  61. fmt.Println(` "rpc" : <rpc interface>,`)
  62. fmt.Println(` "fingerprint" : <fingerprint>`)
  63. fmt.Println(` },`)
  64. fmt.Println(" ...")
  65. fmt.Println(" ],")
  66. fmt.Println(` "tree" : [`)
  67. fmt.Println(` {`)
  68. fmt.Println(` "path" : <path>,`)
  69. fmt.Println(` "branch" : <branch name>,`)
  70. fmt.Println(` "writeable" : <writable flag>`)
  71. fmt.Println(` },`)
  72. fmt.Println(" ...")
  73. fmt.Println(" ]")
  74. fmt.Println("}")
  75. fmt.Println()
  76. }
  77. flag.CommandLine.Parse(os.Args[2:])
  78. if *showHelp {
  79. flag.Usage()
  80. return nil
  81. }
  82. // Load secret and ssl certificate
  83. secret, cert, err := loadSecretAndCert(*secretFile, *certDir)
  84. errorutil.AssertOk(err)
  85. // Create config
  86. cfg := datautil.MergeMaps(config.DefaultTreeConfig)
  87. delete(cfg, config.TreeSecret)
  88. cfg[config.TreeSecret] = secret
  89. // Check for a mapping file
  90. mappingFile := DefaultMappingFile
  91. if len(flag.Args()) > 0 {
  92. mappingFile = flag.Arg(0)
  93. }
  94. // Create the tree object
  95. if tree, err = rufs.NewTree(cfg, cert); err == nil {
  96. // Load mapping file
  97. if ok, _ := fileutil.PathExists(mappingFile); ok {
  98. var conf []byte
  99. fmt.Println(fmt.Sprintf("Using mapping file: %s", mappingFile))
  100. if conf, err = ioutil.ReadFile(mappingFile); err == nil {
  101. tree.SetMapping(string(conf))
  102. }
  103. } else if webExport != nil && *webExport != "" {
  104. err = fmt.Errorf("Need a mapping file when using web export")
  105. } else if fuseMount != nil && *fuseMount != "" {
  106. err = fmt.Errorf("Need a mapping file when using FUSE mount")
  107. } else if dokanMount != nil && *dokanMount != "" {
  108. err = fmt.Errorf("Need a mapping file when using DOKAN mount")
  109. }
  110. if err == nil {
  111. // Check if we want a file system or a terminal
  112. if webExport != nil && *webExport != "" {
  113. err = setupWebExport(webExport, tree, certDir)
  114. } else if fuseMount != nil && *fuseMount != "" {
  115. err = setupFuseMount(fuseMount, tree)
  116. } else if dokanMount != nil && *dokanMount != "" {
  117. err = setupDokanMount(dokanMount, tree)
  118. } else {
  119. // Create the terminal
  120. tt := term.NewTreeTerm(tree, os.Stdout)
  121. // Add special store config command only available in the command line version
  122. tt.AddCmd("storeconfig",
  123. "storeconfig [local file]", "Store the current tree mapping in a local file",
  124. func(tt *term.TreeTerm, arg ...string) (string, error) {
  125. mf := DefaultMappingFile
  126. if len(arg) > 0 {
  127. mf = arg[0]
  128. }
  129. return "", ioutil.WriteFile(mf, []byte(tree.Config()), 0600)
  130. })
  131. // Run the terminal
  132. clt, err := termutil.NewConsoleLineTerminal(os.Stdout)
  133. if err == nil {
  134. isExitLine := func(s string) bool {
  135. return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
  136. }
  137. // Add history functionality
  138. clt, err = termutil.AddHistoryMixin(clt, ".rufs_client_history",
  139. func(s string) bool {
  140. return isExitLine(s)
  141. })
  142. if err == nil {
  143. dictChooser := func(lineWords []string,
  144. dictCache map[string]termutil.Dict) (termutil.Dict, error) {
  145. // Simple dict chooser 1st level are available commands
  146. // 2nd level are the contents of the current directory
  147. if len(lineWords) <= 1 {
  148. return dictCache["cmds"], nil
  149. }
  150. var suggestions []string
  151. if _, fis, err := tree.Dir(tt.CurrentDir(), "", false,
  152. false); err == nil {
  153. for _, f := range fis[0] {
  154. suggestions = append(suggestions, f.Name())
  155. }
  156. }
  157. return termutil.NewWordListDict(suggestions), nil
  158. }
  159. dict := termutil.NewMultiWordDict(dictChooser, map[string]termutil.Dict{
  160. "cmds": termutil.NewWordListDict(tt.Cmds()),
  161. })
  162. // Add auto complete
  163. clt, err = termutil.AddAutoCompleteMixin(clt, dict)
  164. if err == nil {
  165. if err = clt.StartTerm(); err == nil {
  166. var line string
  167. defer clt.StopTerm()
  168. fmt.Println("Type 'q' or 'quit' to exit the shell and '?' to get help")
  169. line, err = clt.NextLine()
  170. for err == nil && !isExitLine(line) {
  171. // Process the entered line
  172. res, terr := tt.Run(line)
  173. if res != "" {
  174. clt.WriteString(fmt.Sprintln(res))
  175. }
  176. if terr != nil {
  177. clt.WriteString(fmt.Sprintln(terr.Error()))
  178. }
  179. line, err = clt.NextLine()
  180. }
  181. }
  182. }
  183. }
  184. }
  185. }
  186. }
  187. }
  188. return err
  189. }