eliasdb.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. "time"
  37. "devt.de/krotik/common/errorutil"
  38. "devt.de/krotik/common/fileutil"
  39. "devt.de/krotik/common/termutil"
  40. "devt.de/krotik/eliasdb/api"
  41. "devt.de/krotik/eliasdb/config"
  42. "devt.de/krotik/eliasdb/console"
  43. "devt.de/krotik/eliasdb/graph"
  44. "devt.de/krotik/eliasdb/server"
  45. )
  46. func main() {
  47. // Initialize the default command line parser
  48. flag.CommandLine.Init(os.Args[0], flag.ContinueOnError)
  49. // Define default usage message
  50. flag.Usage = func() {
  51. // Print usage for tool selection
  52. fmt.Println(fmt.Sprintf("Usage of %s <tool>", os.Args[0]))
  53. fmt.Println()
  54. fmt.Println("EliasDB graph based database")
  55. fmt.Println()
  56. fmt.Println("Available commands:")
  57. fmt.Println()
  58. fmt.Println(" console EliasDB server console")
  59. fmt.Println(" server Start EliasDB server")
  60. fmt.Println()
  61. fmt.Println(fmt.Sprintf("Use %s <command> -help for more information about a given command.", os.Args[0]))
  62. fmt.Println()
  63. }
  64. // Parse the command bit
  65. err := flag.CommandLine.Parse(os.Args[1:])
  66. if len(flag.Args()) > 0 {
  67. arg := flag.Args()[0]
  68. if arg == "server" {
  69. config.LoadConfigFile(config.DefaultConfigFile)
  70. server.StartServerWithSingleOp(handleServerCommandLine)
  71. } else if arg == "console" {
  72. config.LoadConfigFile(config.DefaultConfigFile)
  73. RunCliConsole()
  74. } else {
  75. flag.Usage()
  76. }
  77. } else if err == nil {
  78. flag.Usage()
  79. }
  80. }
  81. /*
  82. RunCliConsole runs the server console on the commandline.
  83. */
  84. func RunCliConsole() {
  85. var err error
  86. // Try to get the server host and port from the config file
  87. chost, cport := getHostPortFromConfig()
  88. host := flag.String("host", chost, "Host of the EliasDB server")
  89. port := flag.String("port", cport, "Port of the EliasDB server")
  90. cmdfile := flag.String("file", "", "Read commands from a file and exit")
  91. cmdline := flag.String("exec", "", "Execute a single line and exit")
  92. showHelp := flag.Bool("help", false, "Show this help message")
  93. flag.Usage = func() {
  94. fmt.Println()
  95. fmt.Println(fmt.Sprintf("Usage of %s console [options]", os.Args[0]))
  96. fmt.Println()
  97. flag.PrintDefaults()
  98. fmt.Println()
  99. }
  100. flag.CommandLine.Parse(os.Args[2:])
  101. if *showHelp {
  102. flag.Usage()
  103. return
  104. }
  105. if *cmdfile == "" && *cmdline == "" {
  106. fmt.Println(fmt.Sprintf("EliasDB %v - Console",
  107. config.ProductVersion))
  108. }
  109. var clt termutil.ConsoleLineTerminal
  110. isExitLine := func(s string) bool {
  111. return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
  112. }
  113. clt, err = termutil.NewConsoleLineTerminal(os.Stdout)
  114. if *cmdfile != "" {
  115. var file *os.File
  116. // Read commands from a file
  117. file, err = os.Open(*cmdfile)
  118. if err == nil {
  119. defer file.Close()
  120. clt, err = termutil.AddFileReadingWrapper(clt, file, true)
  121. }
  122. } else if *cmdline != "" {
  123. var buf bytes.Buffer
  124. buf.WriteString(fmt.Sprintln(*cmdline))
  125. // Read commands from a single line
  126. clt, err = termutil.AddFileReadingWrapper(clt, &buf, true)
  127. } else {
  128. // Add history functionality
  129. histfile := filepath.Join(filepath.Dir(os.Args[0]), ".eliasdb_console_history")
  130. clt, err = termutil.AddHistoryMixin(clt, histfile,
  131. func(s string) bool {
  132. return isExitLine(s)
  133. })
  134. }
  135. if err == nil {
  136. // Create the console object
  137. con := console.NewConsole(fmt.Sprintf("https://%s:%s", *host, *port), os.Stdout,
  138. func() (string, string) {
  139. // Login function
  140. line, err := clt.NextLinePrompt("Login username: ", 0x0)
  141. user := strings.TrimRight(line, "\r\n")
  142. errorutil.AssertOk(err)
  143. pass, err := clt.NextLinePrompt("Password: ", '*')
  144. errorutil.AssertOk(err)
  145. return user, pass
  146. },
  147. func() string {
  148. // Enter password function
  149. var err error
  150. var pass, pass2 string
  151. pass2 = "x"
  152. for pass != pass2 {
  153. pass, err = clt.NextLinePrompt("Password: ", '*')
  154. errorutil.AssertOk(err)
  155. pass2, err = clt.NextLinePrompt("Re-type password: ", '*')
  156. errorutil.AssertOk(err)
  157. if pass != pass2 {
  158. clt.WriteString(fmt.Sprintln("Passwords don't match"))
  159. }
  160. }
  161. return pass
  162. },
  163. func(args []string, exportBuf *bytes.Buffer) error {
  164. // Export data to a chosen file
  165. filename := "export.out"
  166. if len(args) > 0 {
  167. filename = args[0]
  168. }
  169. return ioutil.WriteFile(filename, exportBuf.Bytes(), 0666)
  170. })
  171. // Start the console
  172. if err = clt.StartTerm(); err == nil {
  173. var line string
  174. defer clt.StopTerm()
  175. if *cmdfile == "" && *cmdline == "" {
  176. fmt.Println("Type 'q' or 'quit' to exit the shell and '?' to get help")
  177. }
  178. line, err = clt.NextLine()
  179. for err == nil && !isExitLine(line) {
  180. _, cerr := con.Run(line)
  181. if cerr != nil {
  182. // Output any error
  183. fmt.Fprintln(clt, cerr.Error())
  184. }
  185. line, err = clt.NextLine()
  186. }
  187. }
  188. }
  189. if err != nil {
  190. fmt.Println(err.Error())
  191. }
  192. }
  193. /*
  194. getHostPortFromConfig gets the host and port from the config file or the
  195. default config.
  196. */
  197. func getHostPortFromConfig() (string, string) {
  198. host := fileutil.ConfStr(config.DefaultConfig, config.HTTPSHost)
  199. port := fileutil.ConfStr(config.DefaultConfig, config.HTTPSPort)
  200. if ok, _ := fileutil.PathExists(config.DefaultConfigFile); ok {
  201. cfg, _ := fileutil.LoadConfig(config.DefaultConfigFile, config.DefaultConfig)
  202. if cfg != nil {
  203. host = fileutil.ConfStr(cfg, config.HTTPSHost)
  204. port = fileutil.ConfStr(cfg, config.HTTPSPort)
  205. }
  206. }
  207. return host, port
  208. }
  209. /*
  210. handleServerCommandLine handles all command line options for the server
  211. */
  212. func handleServerCommandLine(gm *graph.Manager) bool {
  213. var err error
  214. var ecalConsole *bool
  215. importDb := flag.String("import", "", "Import a database from a zip file")
  216. exportDb := flag.String("export", "", "Export the current database to a zip file")
  217. if config.Bool(config.EnableECALScripts) {
  218. ecalConsole = flag.Bool("ecal-console", false, "Start an interactive interpreter console for ECAL")
  219. }
  220. noServ := flag.Bool("no-serv", false, "Do not start the server after initialization")
  221. showHelp := flag.Bool("help", false, "Show this help message")
  222. flag.Usage = func() {
  223. fmt.Println()
  224. fmt.Println(fmt.Sprintf("Usage of %s server [options]", os.Args[0]))
  225. fmt.Println()
  226. flag.PrintDefaults()
  227. fmt.Println()
  228. }
  229. flag.CommandLine.Parse(os.Args[2:])
  230. if *showHelp {
  231. flag.Usage()
  232. return true
  233. }
  234. if *importDb != "" {
  235. var zipFile *zip.ReadCloser
  236. fmt.Println("Importing from:", *importDb)
  237. if zipFile, err = zip.OpenReader(*importDb); err == nil {
  238. defer zipFile.Close()
  239. for _, file := range zipFile.File {
  240. var in io.Reader
  241. if !file.FileInfo().IsDir() {
  242. part := strings.TrimSuffix(filepath.Base(file.Name), filepath.Ext(file.Name))
  243. fmt.Println(fmt.Sprintf("Importing %s to partition %s", file.Name, part))
  244. if in, err = file.Open(); err == nil {
  245. err = graph.ImportPartition(in, part, gm)
  246. }
  247. if err != nil {
  248. break
  249. }
  250. }
  251. }
  252. }
  253. }
  254. if *exportDb != "" {
  255. var zipFile *os.File
  256. fmt.Println("Exporting to:", *exportDb)
  257. if zipFile, err = os.Create(*exportDb); err == nil {
  258. defer zipFile.Close()
  259. zipWriter := zip.NewWriter(zipFile)
  260. defer zipWriter.Close()
  261. for _, part := range gm.Partitions() {
  262. var exportFile io.Writer
  263. name := fmt.Sprintf("%s.json", part)
  264. fmt.Println(fmt.Sprintf("Exporting partition %s to %s", part, name))
  265. if exportFile, err = zipWriter.Create(name); err == nil {
  266. err = graph.ExportPartition(exportFile, part, gm)
  267. }
  268. if err != nil {
  269. break
  270. }
  271. }
  272. }
  273. }
  274. if ecalConsole != nil && *ecalConsole {
  275. var term termutil.ConsoleLineTerminal
  276. isExitLine := func(s string) bool {
  277. return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
  278. }
  279. term, err = termutil.NewConsoleLineTerminal(os.Stdout)
  280. if err == nil {
  281. term, err = termutil.AddHistoryMixin(term, "", isExitLine)
  282. if err == nil {
  283. tid := api.SI.Interpreter.RuntimeProvider.NewThreadID()
  284. runECALConsole := func(delay int) {
  285. defer term.StopTerm()
  286. time.Sleep(time.Duration(delay) * time.Millisecond)
  287. term.WriteString(fmt.Sprintln("Type 'q' or 'quit' to exit the shell and '?' to get help"))
  288. line, err := term.NextLine()
  289. for err == nil && !isExitLine(line) {
  290. trimmedLine := strings.TrimSpace(line)
  291. api.SI.Interpreter.HandleInput(term, trimmedLine, tid)
  292. line, err = term.NextLine()
  293. }
  294. }
  295. if err = term.StartTerm(); err == nil {
  296. if *noServ {
  297. runECALConsole(0)
  298. } else {
  299. go runECALConsole(3000)
  300. }
  301. }
  302. }
  303. }
  304. }
  305. if err != nil {
  306. fmt.Println(err.Error())
  307. return true
  308. }
  309. return *noServ
  310. }