eliasdb.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. /*
  2. * EliasDB
  3. *
  4. * Copyright 2016 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the Mozilla Public
  7. * License, v. 2.0. If a copy of the MPL was not distributed with this
  8. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  9. */
  10. /*
  11. EliasDB is a graph based database which aims to provide a lightweight solution
  12. for projects which want to store their data as a graph.
  13. Features:
  14. - Build on top of a fast key-value store which supports transactions and memory-only storage.
  15. - Data is stored in nodes (key-value objects) which are connected via edges.
  16. - Stored graphs can be separated via partitions.
  17. - Stored graphs support cascading deletions - delete one node and all its "children".
  18. - All stored data is indexed and can be quickly searched via a full text phrase search.
  19. - For more complex queries EliasDB has an own query language called EQL with an sql-like syntax.
  20. - Written in Go from scratch. No third party libraries were used apart from Go's standard library.
  21. - The database can be embedded or used as a standalone application.
  22. - When used as a standalone application it comes with an internal HTTPS webserver which provides a REST API and a basic file server.
  23. - When used as an embedded database it supports transactions with rollbacks, iteration of data and rule based consistency management.
  24. */
  25. package main
  26. import (
  27. "archive/zip"
  28. "bytes"
  29. "flag"
  30. "fmt"
  31. "io"
  32. "io/ioutil"
  33. "os"
  34. "path/filepath"
  35. "strings"
  36. "devt.de/krotik/common/errorutil"
  37. "devt.de/krotik/common/fileutil"
  38. "devt.de/krotik/common/termutil"
  39. "devt.de/krotik/eliasdb/config"
  40. "devt.de/krotik/eliasdb/console"
  41. "devt.de/krotik/eliasdb/graph"
  42. "devt.de/krotik/eliasdb/server"
  43. )
  44. func main() {
  45. // Initialize the default command line parser
  46. flag.CommandLine.Init(os.Args[0], flag.ContinueOnError)
  47. // Define default usage message
  48. flag.Usage = func() {
  49. // Print usage for tool selection
  50. fmt.Println(fmt.Sprintf("Usage of %s <tool>", os.Args[0]))
  51. fmt.Println()
  52. fmt.Println("EliasDB graph based database")
  53. fmt.Println()
  54. fmt.Println("Available commands:")
  55. fmt.Println()
  56. fmt.Println(" console EliasDB server console")
  57. fmt.Println(" server Start EliasDB server")
  58. fmt.Println()
  59. fmt.Println(fmt.Sprintf("Use %s <command> -help for more information about a given command.", os.Args[0]))
  60. fmt.Println()
  61. }
  62. // Parse the command bit
  63. err := flag.CommandLine.Parse(os.Args[1:])
  64. if len(flag.Args()) > 0 {
  65. arg := flag.Args()[0]
  66. if arg == "server" {
  67. config.LoadConfigFile(config.DefaultConfigFile)
  68. server.StartServerWithSingleOp(handleServerCommandLine)
  69. } else if arg == "console" {
  70. config.LoadConfigFile(config.DefaultConfigFile)
  71. RunCliConsole()
  72. } else {
  73. flag.Usage()
  74. }
  75. } else if err == nil {
  76. flag.Usage()
  77. }
  78. }
  79. /*
  80. RunCliConsole runs the server console on the commandline.
  81. */
  82. func RunCliConsole() {
  83. var err error
  84. // Try to get the server host and port from the config file
  85. chost, cport := getHostPortFromConfig()
  86. host := flag.String("host", chost, "Host of the EliasDB server")
  87. port := flag.String("port", cport, "Port of the EliasDB server")
  88. cmdfile := flag.String("file", "", "Read commands from a file and exit")
  89. cmdline := flag.String("exec", "", "Execute a single line and exit")
  90. showHelp := flag.Bool("help", false, "Show this help message")
  91. flag.Usage = func() {
  92. fmt.Println()
  93. fmt.Println(fmt.Sprintf("Usage of %s console [options]", os.Args[0]))
  94. fmt.Println()
  95. flag.PrintDefaults()
  96. fmt.Println()
  97. }
  98. flag.CommandLine.Parse(os.Args[2:])
  99. if *showHelp {
  100. flag.Usage()
  101. return
  102. }
  103. if *cmdfile == "" && *cmdline == "" {
  104. fmt.Println(fmt.Sprintf("EliasDB %v - Console",
  105. config.ProductVersion))
  106. }
  107. var clt termutil.ConsoleLineTerminal
  108. isExitLine := func(s string) bool {
  109. return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
  110. }
  111. clt, err = termutil.NewConsoleLineTerminal(os.Stdout)
  112. if *cmdfile != "" {
  113. var file *os.File
  114. // Read commands from a file
  115. file, err = os.Open(*cmdfile)
  116. if err == nil {
  117. defer file.Close()
  118. clt, err = termutil.AddFileReadingWrapper(clt, file, true)
  119. }
  120. } else if *cmdline != "" {
  121. var buf bytes.Buffer
  122. buf.WriteString(fmt.Sprintln(*cmdline))
  123. // Read commands from a single line
  124. clt, err = termutil.AddFileReadingWrapper(clt, &buf, true)
  125. } else {
  126. // Add history functionality
  127. histfile := filepath.Join(filepath.Dir(os.Args[0]), ".eliasdb_console_history")
  128. clt, err = termutil.AddHistoryMixin(clt, histfile,
  129. func(s string) bool {
  130. return isExitLine(s)
  131. })
  132. }
  133. if err == nil {
  134. // Create the console object
  135. con := console.NewConsole(fmt.Sprintf("https://%s:%s", *host, *port), os.Stdout,
  136. func() (string, string) {
  137. // Login function
  138. line, err := clt.NextLinePrompt("Login username: ", 0x0)
  139. user := strings.TrimRight(line, "\r\n")
  140. errorutil.AssertOk(err)
  141. pass, err := clt.NextLinePrompt("Password: ", '*')
  142. errorutil.AssertOk(err)
  143. return user, pass
  144. },
  145. func() string {
  146. // Enter password function
  147. var err error
  148. var pass, pass2 string
  149. pass2 = "x"
  150. for pass != pass2 {
  151. pass, err = clt.NextLinePrompt("Password: ", '*')
  152. errorutil.AssertOk(err)
  153. pass2, err = clt.NextLinePrompt("Re-type password: ", '*')
  154. errorutil.AssertOk(err)
  155. if pass != pass2 {
  156. clt.WriteString(fmt.Sprintln("Passwords don't match"))
  157. }
  158. }
  159. return pass
  160. },
  161. func(args []string, exportBuf *bytes.Buffer) error {
  162. // Export data to a chosen file
  163. filename := "export.out"
  164. if len(args) > 0 {
  165. filename = args[0]
  166. }
  167. return ioutil.WriteFile(filename, exportBuf.Bytes(), 0666)
  168. })
  169. // Start the console
  170. if err = clt.StartTerm(); err == nil {
  171. var line string
  172. defer clt.StopTerm()
  173. if *cmdfile == "" && *cmdline == "" {
  174. fmt.Println("Type 'q' or 'quit' to exit the shell and '?' to get help")
  175. }
  176. line, err = clt.NextLine()
  177. for err == nil && !isExitLine(line) {
  178. _, cerr := con.Run(line)
  179. if cerr != nil {
  180. // Output any error
  181. fmt.Fprintln(clt, cerr.Error())
  182. }
  183. line, err = clt.NextLine()
  184. }
  185. }
  186. }
  187. if err != nil {
  188. fmt.Println(err.Error())
  189. }
  190. }
  191. /*
  192. getHostPortFromConfig gets the host and port from the config file or the
  193. default config.
  194. */
  195. func getHostPortFromConfig() (string, string) {
  196. host := fileutil.ConfStr(config.DefaultConfig, config.HTTPSHost)
  197. port := fileutil.ConfStr(config.DefaultConfig, config.HTTPSPort)
  198. if ok, _ := fileutil.PathExists(config.DefaultConfigFile); ok {
  199. cfg, _ := fileutil.LoadConfig(config.DefaultConfigFile, config.DefaultConfig)
  200. if cfg != nil {
  201. host = fileutil.ConfStr(cfg, config.HTTPSHost)
  202. port = fileutil.ConfStr(cfg, config.HTTPSPort)
  203. }
  204. }
  205. return host, port
  206. }
  207. /*
  208. handleServerCommandLine handles all command line options for the server
  209. */
  210. func handleServerCommandLine(gm *graph.Manager) bool {
  211. var err error
  212. importDb := flag.String("import", "", "Import a database from a zip file")
  213. exportDb := flag.String("export", "", "Export the current database to a zip file")
  214. noServ := flag.Bool("no-serv", false, "Do not start the server after initialization")
  215. showHelp := flag.Bool("help", false, "Show this help message")
  216. flag.Usage = func() {
  217. fmt.Println()
  218. fmt.Println(fmt.Sprintf("Usage of %s server [options]", os.Args[0]))
  219. fmt.Println()
  220. flag.PrintDefaults()
  221. fmt.Println()
  222. }
  223. flag.CommandLine.Parse(os.Args[2:])
  224. if *showHelp {
  225. flag.Usage()
  226. return true
  227. }
  228. if *importDb != "" {
  229. var zipFile *zip.ReadCloser
  230. fmt.Println("Importing from:", *importDb)
  231. if zipFile, err = zip.OpenReader(*importDb); err == nil {
  232. defer zipFile.Close()
  233. for _, file := range zipFile.File {
  234. var in io.Reader
  235. if !file.FileInfo().IsDir() {
  236. part := strings.TrimSuffix(filepath.Base(file.Name), filepath.Ext(file.Name))
  237. fmt.Println(fmt.Sprintf("Importing %s to partition %s", file.Name, part))
  238. if in, err = file.Open(); err == nil {
  239. err = graph.ImportPartition(in, part, gm)
  240. }
  241. if err != nil {
  242. break
  243. }
  244. }
  245. }
  246. }
  247. }
  248. if *exportDb != "" {
  249. var zipFile *os.File
  250. fmt.Println("Exporting to:", *exportDb)
  251. if zipFile, err = os.Create(*exportDb); err == nil {
  252. defer zipFile.Close()
  253. zipWriter := zip.NewWriter(zipFile)
  254. defer zipWriter.Close()
  255. for _, part := range gm.Partitions() {
  256. var exportFile io.Writer
  257. name := fmt.Sprintf("%s.json", part)
  258. fmt.Println(fmt.Sprintf("Exporting partition %s to %s", part, name))
  259. if exportFile, err = zipWriter.Create(name); err == nil {
  260. err = graph.ExportPartition(exportFile, part, gm)
  261. }
  262. if err != nil {
  263. break
  264. }
  265. }
  266. }
  267. }
  268. if err != nil {
  269. fmt.Println(err.Error())
  270. return true
  271. }
  272. return *noServ
  273. }