eliasdb.go 12 KB

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