gosl.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. // gosl is a basic example of how to develop external web services for Second Life/OpenSimulator using the Go programming language.
  2. package main
  3. import (
  4. "bufio"
  5. "compress/bzip2"
  6. "encoding/csv"
  7. "encoding/json"
  8. flag "github.com/spf13/pflag"
  9. "fmt"
  10. "github.com/dgraph-io/badger"
  11. "github.com/dgraph-io/badger/options"
  12. "github.com/fsnotify/fsnotify"
  13. "github.com/op/go-logging"
  14. "github.com/spf13/viper"
  15. "github.com/syndtr/goleveldb/leveldb"
  16. "github.com/syndtr/goleveldb/leveldb/util"
  17. "github.com/tidwall/buntdb"
  18. "gopkg.in/natefinch/lumberjack.v2"
  19. "io"
  20. "net/http"
  21. "net/http/fcgi"
  22. "os"
  23. "path/filepath"
  24. "regexp"
  25. "runtime"
  26. "strings"
  27. "time"
  28. )
  29. const NullUUID = "00000000-0000-0000-0000-000000000000" // always useful when we deal with SL/OpenSimulator...
  30. const databaseName = "gosl-database.db" // for BuntDB
  31. // Logging setup.
  32. var log = logging.MustGetLogger("gosl") // configuration for the go-logging logger, must be available everywhere
  33. var logFormat logging.Formatter
  34. // Opt is used for Badger database setup.
  35. var Opt badger.Options
  36. // AvatarUUID is the type that we store in the database; we keep a record from which grid it came from.
  37. type avatarUUID struct {
  38. UUID string // needs to be capitalised for JSON marshalling (it has to do with the way it works)
  39. Grid string
  40. }
  41. /*
  42. .__
  43. _____ _____ |__| ____
  44. / \\__ \ | |/ \
  45. | Y Y \/ __ \| | | \
  46. |__|_| (____ /__|___| /
  47. \/ \/ \/
  48. */
  49. // Configuration options
  50. type goslConfigOptions struct {
  51. BATCH_BLOCK int // how many entries to write to the database as a block; the bigger, the faster, but the more memory it consumes
  52. noMemory, isServer, isShell *bool
  53. myDir, myPort, importFilename, database *string
  54. dbNamePath string // for BuntDB
  55. logFilename string // for logs
  56. maxSize, maxBackups, maxAge int // logs configuration option
  57. }
  58. var goslConfig goslConfigOptions
  59. // loadConfiguration reads our configuration from a config.toml file
  60. func loadConfiguration() {
  61. fmt.Print("Reading gosl-basic configuration:") // note that we might not have go-logging active as yet, so we use fmt
  62. // Open our config file and extract relevant data from there
  63. err := viper.ReadInConfig() // Find and read the config file
  64. if err != nil {
  65. fmt.Println("Error reading config file:", err)
  66. return // we might still get away with this!
  67. }
  68. viper.SetDefault("config.BATCH_BLOCK", 100000) // NOTE(gwyneth): the authors of say that 100000 is way too much for Badger // NOTE(gwyneth): let's see what happens with BuntDB
  69. goslConfig.BATCH_BLOCK = viper.GetInt("config.BATCH_BLOCK")
  70. viper.SetDefault("config.myPort", 3000)
  71. *goslConfig.myPort = viper.GetString("config.myPort")
  72. viper.SetDefault("config.myDir", "slkvdb")
  73. *goslConfig.myDir = viper.GetString("config.myDir")
  74. viper.SetDefault("config.isServer", false)
  75. *goslConfig.isServer = viper.GetBool("config.isServer")
  76. viper.SetDefault("config.isShell", false)
  77. *goslConfig.isShell = viper.GetBool("config.isShell")
  78. viper.SetDefault("config.database", "badger") // currently, badger, boltdb, leveldb
  79. *goslConfig.database = viper.GetString("config.database")
  80. viper.SetDefault("config.importFilename", "") // must be empty by default
  81. *goslConfig.importFilename = viper.GetString("config.importFilename")
  82. viper.SetDefault("config.noMemory", false)
  83. *goslConfig.noMemory = viper.GetBool("config.noMemory")
  84. // Logging options
  85. viper.SetDefault("config.logFilename", "gosl.log")
  86. goslConfig.logFilename = viper.GetString("config.logFilename")
  87. viper.SetDefault("config.maxSize", 10)
  88. goslConfig.maxSize = viper.GetInt("config.maxSize")
  89. viper.SetDefault("config.maxBackups", 3)
  90. goslConfig.maxBackups = viper.GetInt("config.maxBackups")
  91. viper.SetDefault("config.maxAge", 28)
  92. goslConfig.maxAge = viper.GetInt("config.maxAge")
  93. }
  94. // main() starts here.
  95. func main() {
  96. // Flag setup; can be overridden by config file (I need to fix this to be the oher way round).
  97. goslConfig.myPort = flag.String("port", "3000", "Server port")
  98. goslConfig.myDir = flag.String("dir", "slkvdb", "Directory where database files are stored")
  99. goslConfig.isServer = flag.Bool("server", false, "Run as server on port " + *goslConfig.myPort)
  100. goslConfig.isShell = flag.Bool("shell", false, "Run as an interactive shell")
  101. goslConfig.importFilename = flag.String("import", "name2key.csv.bz2", "Import database from W-Hat (use the csv.bz2 version)")
  102. goslConfig.database = flag.String("database", "badger", "Database type (badger, buntdb, leveldb)")
  103. goslConfig.noMemory = flag.Bool("nomemory", false, "Attempt to use only disk to save memory on Badger (important for shared webservers)")
  104. // Config viper, which reads in the configuration file every time it's needed.
  105. // Note that we need some hard-coded variables for the path and config file name.
  106. viper.SetConfigName("config")
  107. viper.SetConfigType("toml") // just to make sure; it's the same format as OpenSimulator (or MySQL) config files
  108. viper.AddConfigPath("$HOME/go/src/gosl-basics/") // that's how I have it
  109. viper.AddConfigPath("$HOME/go/src/git.gwynethllewelyn.net/GwynethLlewelyn/gosl-basics/") // that's how you'll have it
  110. viper.AddConfigPath(".") // optionally look for config in the working directory
  111. loadConfiguration()
  112. // default is FastCGI
  113. flag.Parse()
  114. viper.BindPFlags(flag.CommandLine)
  115. // this will allow our configuration file to be 'read on demand'
  116. viper.WatchConfig()
  117. viper.OnConfigChange(func(e fsnotify.Event) {
  118. if *goslConfig.isServer || *goslConfig.isShell {
  119. fmt.Println("Config file changed:", e.Name) // BUG(gwyneth): FastCGI cannot write to output
  120. }
  121. loadConfiguration()
  122. })
  123. // NOTE(gwyneth): We cannot write to stdout if we're running as FastCGI, only to logs!
  124. if *goslConfig.isServer || *goslConfig.isShell {
  125. fmt.Println("gosl is starting...")
  126. }
  127. // Setup the lumberjack rotating logger. This is because we need it for the go-logging logger when writing to files. (20170813)
  128. rotatingLogger := &lumberjack.Logger{
  129. Filename: goslConfig.logFilename,
  130. MaxSize: goslConfig.maxSize, // megabytes
  131. MaxBackups: goslConfig.maxBackups,
  132. MaxAge: goslConfig.maxAge, //days
  133. }
  134. // Set formatting for stderr and file (basically the same).
  135. logFormat := logging.MustStringFormatter(`%{color}%{time:2006/01/02 15:04:05.0} %{shortfile} - %{shortfunc} ▶ %{level:.4s}%{color:reset} %{message}`) // must be initialised or all hell breaks loose
  136. // Setup the go-logging Logger. Do **not** log to stderr if running as FastCGI!
  137. backendFile := logging.NewLogBackend(rotatingLogger, "", 0)
  138. backendFileFormatter := logging.NewBackendFormatter(backendFile, logFormat)
  139. backendFileLeveled := logging.AddModuleLevel(backendFileFormatter)
  140. backendFileLeveled.SetLevel(logging.INFO, "gosl") // we just send debug data to logs if we run as shell
  141. if *goslConfig.isServer || *goslConfig.isShell {
  142. backendStderr := logging.NewLogBackend(os.Stderr, "", 0)
  143. backendStderrFormatter := logging.NewBackendFormatter(backendStderr, logFormat)
  144. backendStderrLeveled := logging.AddModuleLevel(backendStderrFormatter)
  145. if *goslConfig.isShell {
  146. backendStderrLeveled.SetLevel(logging.DEBUG, "gosl") // shell is meant to be for debugging mostly
  147. } else {
  148. backendStderrLeveled.SetLevel(logging.INFO, "gosl")
  149. }
  150. logging.SetBackend(backendStderrLeveled, backendFileLeveled)
  151. } else {
  152. logging.SetBackend(backendFileLeveled) // FastCGI only logs to file
  153. }
  154. // Check if this directory actually exists; if not, create it. Panic if something wrong happens (we cannot proceed without a valid directory for the database to be written
  155. if stat, err := os.Stat(*goslConfig.myDir); err == nil && stat.IsDir() {
  156. // path is a valid directory
  157. log.Infof("Valid directory: %s\n", *goslConfig.myDir)
  158. } else {
  159. // try to create directory
  160. err = os.Mkdir(*goslConfig.myDir, 0700)
  161. checkErrPanic(err) // cannot make directory, panic and exit logging what went wrong
  162. log.Debugf("Created new directory: %s\n", *goslConfig.myDir)
  163. }
  164. if *goslConfig.database == "badger" {
  165. Opt = badger.DefaultOptions
  166. Opt.Dir = *goslConfig.myDir
  167. Opt.ValueDir = Opt.Dir
  168. Opt.TableLoadingMode = options.MemoryMap
  169. //Opt.TableLoadingMode = options.FileIO
  170. if *goslConfig.noMemory {
  171. // Opt.TableLoadingMode = options.FileIO // use standard file I/O operations for tables instead of LoadRAM
  172. Opt.TableLoadingMode = options.MemoryMap // MemoryMap indicates that that the file must be memory-mapped - https://github.com/dgraph-io/badger/issues/224#issuecomment-329643771
  173. // Opt.TableLoadingMode = options.FileIO
  174. // Opt.ValueLogFileSize = 1048576
  175. Opt.MaxTableSize = 1048576 // * 12
  176. Opt.LevelSizeMultiplier = 1
  177. Opt.NumMemtables = 1
  178. // Opt.MaxLevels = 10
  179. // Opt.SyncWrites = false
  180. // Opt.NumCompactors = 10
  181. // Opt.NumLevelZeroTables = 10
  182. // Opt.maxBatchSize =
  183. // Opt.maxBatchCount =
  184. goslConfig.BATCH_BLOCK = 1000 // try to import less at each time, it will take longer but hopefully work
  185. log.Info("Trying to avoid too much memory consumption")
  186. }
  187. }
  188. // Do some testing to see if the database is available
  189. const testAvatarName = "Nobody Here"
  190. var err error
  191. log.Info("gosl started and logging is set up. Proceeding to test database (" + *goslConfig.database + ") at " + *goslConfig.myDir)
  192. var testValue = avatarUUID{ NullUUID, "all grids" }
  193. jsonTestValue, err := json.Marshal(testValue)
  194. checkErrPanic(err) // something went VERY wrong
  195. if *goslConfig.database == "badger" {
  196. kv, err := badger.Open(Opt)
  197. checkErrPanic(err) // should probably panic, cannot prep new database
  198. txn := kv.NewTransaction(true)
  199. err = txn.Set([]byte(testAvatarName), jsonTestValue)
  200. checkErrPanic(err)
  201. err = txn.Commit(nil)
  202. checkErrPanic(err)
  203. log.Debugf("badger SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  204. kv.Close()
  205. } else if *goslConfig.database == "buntdb" {
  206. /* NOTE(gwyneth): this fails because pointers to strings do not implement len(). Duh!
  207. if *goslConfig.myDir[len(*goslConfig.myDir)-1] != os.PathSeparator {
  208. *goslConfig.myDir = append(*goslConfig.myDir + os.PathSeparator
  209. } */
  210. goslConfig.dbNamePath = *goslConfig.myDir + string(os.PathSeparator) + databaseName
  211. db, err := buntdb.Open(goslConfig.dbNamePath)
  212. checkErrPanic(err)
  213. err = db.Update(func(tx *buntdb.Tx) error {
  214. _, _, err := tx.Set(testAvatarName, string(jsonTestValue), nil)
  215. return err
  216. })
  217. checkErr(err)
  218. log.Debugf("buntdb SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  219. db.Close()
  220. } else if *goslConfig.database == "leveldb" {
  221. goslConfig.dbNamePath = *goslConfig.myDir + string(os.PathSeparator) + databaseName
  222. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  223. checkErrPanic(err)
  224. err = db.Put([]byte(testAvatarName), jsonTestValue, nil)
  225. checkErrPanic(err)
  226. log.Debugf("leveldb SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  227. db.Close()
  228. }
  229. // common to all databases:
  230. key, grid := searchKVname(testAvatarName)
  231. log.Debugf("GET '%s' returned '%s' [grid '%s']\n", testAvatarName, key, grid)
  232. log.Info("KV database seems fine.")
  233. if *goslConfig.importFilename != "" {
  234. log.Info("Attempting to import", *goslConfig.importFilename, "...")
  235. importDatabase(*goslConfig.importFilename)
  236. log.Info("Database finished import.")
  237. }
  238. if *goslConfig.isShell {
  239. log.Info("Starting to run as interactive shell")
  240. reader := bufio.NewReader(os.Stdin)
  241. fmt.Println("Ctrl-C to quit.")
  242. var err error // to avoid assigning text in a different scope (this is a bit awkward, but that's the problem with bi-assignment)
  243. var checkInput, avatarName, avatarKey, gridName string
  244. for {
  245. // Prompt and read
  246. fmt.Print("Enter avatar name or UUID: ")
  247. checkInput, err = reader.ReadString('\n')
  248. checkErr(err)
  249. checkInput = strings.TrimRight(checkInput, "\r\n")
  250. // fmt.Printf("Ok, got %s length is %d and UUID is %v\n", checkInput, len(checkInput), isValidUUID(checkInput))
  251. if (len(checkInput) == 36) && isValidUUID(checkInput) {
  252. avatarName, gridName = searchKVUUID(checkInput)
  253. avatarKey = checkInput
  254. } else {
  255. avatarKey, gridName = searchKVname(checkInput)
  256. avatarName = checkInput
  257. }
  258. if avatarName != NullUUID && avatarKey != NullUUID {
  259. fmt.Println(avatarName, "which has UUID:", avatarKey, "comes from grid:", gridName)
  260. } else {
  261. fmt.Println("Sorry, unknown input", checkInput)
  262. }
  263. }
  264. // never leaves until Ctrl-C
  265. }
  266. // set up routing.
  267. // NOTE(gwyneth): one function only because FastCGI seems to have problems with multiple handlers.
  268. http.HandleFunc("/", handler)
  269. log.Info("Directory for database:", *goslConfig.myDir)
  270. if (*goslConfig.isServer) {
  271. log.Info("Starting to run as web server on port " + *goslConfig.myPort)
  272. err := http.ListenAndServe(":" + *goslConfig.myPort, nil) // set listen port
  273. checkErrPanic(err) // if it can't listen to all the above, then it has to abort anyway
  274. } else {
  275. // default is to run as FastCGI!
  276. // works like a charm thanks to http://www.dav-muz.net/blog/2013/09/how-to-use-go-and-fastcgi/
  277. log.Debug("http.DefaultServeMux is", http.DefaultServeMux)
  278. if err := fcgi.Serve(nil, nil); err != nil {
  279. checkErrPanic(err)
  280. }
  281. }
  282. // we should never have reached this point!
  283. log.Error("Unknown usage! This application may run as a standalone server, as FastCGI application, or as an interactive shell")
  284. if *goslConfig.isServer || *goslConfig.isShell {
  285. flag.PrintDefaults()
  286. }
  287. }
  288. // handler deals with incoming queries and/or associates avatar names with keys depending on parameters.
  289. // Basically we check if both an avatar name and a UUID key has been received: if yes, this means a new entry;
  290. // if just the avatar name was received, it means looking up its key;
  291. // if just the key was received, it means looking up the name (not necessary since llKey2Name does that, but it's just to illustrate);
  292. // if nothing is received, then return an error
  293. func handler(w http.ResponseWriter, r *http.Request) {
  294. if err := r.ParseForm(); err != nil {
  295. logErrHTTP(w, http.StatusNotFound, "No avatar and/or UUID received")
  296. return
  297. }
  298. // test first if this comes from Second Life or OpenSimulator
  299. /*
  300. if r.Header.Get("X-Secondlife-Region") == "" {
  301. logErrHTTP(w, http.StatusForbidden, "Sorry, this application only works inside Second Life.")
  302. return
  303. }
  304. */
  305. name := r.Form.Get("name") // can be empty
  306. key := r.Form.Get("key") // can be empty
  307. compat := r.Form.Get("compat") // compatibility mode with W-Hat
  308. var valueToInsert avatarUUID
  309. messageToSL := "" // this is what we send back to SL - defined here due to scope issues.
  310. if name != "" {
  311. if key != "" {
  312. // we received both: add a new entry
  313. valueToInsert.UUID = key
  314. valueToInsert.Grid = r.Header.Get("X-Secondlife-Shard")
  315. jsonValueToInsert, err := json.Marshal(valueToInsert)
  316. checkErr(err)
  317. if *goslConfig.database == "badger" {
  318. kv, err := badger.Open(Opt)
  319. checkErrPanic(err) // should probably panic
  320. txn := kv.NewTransaction(true)
  321. defer txn.Discard()
  322. err = txn.Set([]byte(name), jsonValueToInsert)
  323. checkErrPanic(err)
  324. err = txn.Commit(nil)
  325. checkErrPanic(err)
  326. kv.Close()
  327. } else if *goslConfig.database == "buntdb" {
  328. db, err := buntdb.Open(goslConfig.dbNamePath)
  329. checkErrPanic(err)
  330. defer db.Close()
  331. err = db.Update(func(tx *buntdb.Tx) error {
  332. _, _, err := tx.Set(name, string(jsonValueToInsert), nil)
  333. return err
  334. })
  335. checkErr(err)
  336. } else if *goslConfig.database == "leveldb" {
  337. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  338. checkErrPanic(err)
  339. err = db.Put([]byte(name), jsonValueToInsert, nil)
  340. checkErrPanic(err)
  341. db.Close()
  342. }
  343. messageToSL += "Added new entry for '" + name + "' which is: " + valueToInsert.UUID + " from grid: '" + valueToInsert.Grid + "'"
  344. } else {
  345. // we received a name: look up its UUID key and grid.
  346. key, grid := searchKVname(name)
  347. if compat == "false" {
  348. messageToSL += "UUID for '" + name + "' is: " + key + " from grid: '" + grid + "'"
  349. } else { // empty also means true!
  350. messageToSL += key
  351. }
  352. }
  353. } else if key != "" {
  354. // in this scenario, we have the UUID key but no avatar name: do the equivalent of a llKey2Name (slow)
  355. name, grid := searchKVUUID(key)
  356. if compat == "false" {
  357. messageToSL += "Avatar name for " + key + "' is '" + name + "' on grid: '" + grid + "'"
  358. } else { // empty also means true!
  359. messageToSL += name
  360. }
  361. } else {
  362. // neither UUID key nor avatar received, this is an error
  363. logErrHTTP(w, http.StatusNotFound, "Empty avatar name and UUID key received, cannot proceed")
  364. return
  365. }
  366. w.WriteHeader(http.StatusOK)
  367. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  368. fmt.Fprintf(w, messageToSL)
  369. }
  370. // searchKVname searches the KV database for an avatar name.
  371. func searchKVname(avatarName string) (UUID string, grid string) {
  372. var val = avatarUUID{ NullUUID, "" }
  373. time_start := time.Now()
  374. var err error // to deal with scope issues
  375. if *goslConfig.database == "badger" {
  376. kv, err := badger.Open(Opt)
  377. checkErrPanic(err)
  378. defer kv.Close()
  379. err = kv.View(func(txn *badger.Txn) error {
  380. item, err := txn.Get([]byte(avatarName))
  381. if err != nil {
  382. return err
  383. }
  384. data, err := item.Value()
  385. if err != nil {
  386. log.Errorf("Error '%s' while getting data from %v\n", err, item)
  387. return err
  388. }
  389. err = json.Unmarshal(data, &val)
  390. if err != nil {
  391. log.Errorf("Error while unparsing UUID for name: '%s' (%v)\n", avatarName, err)
  392. return err
  393. }
  394. return nil
  395. })
  396. } else if *goslConfig.database == "buntdb" {
  397. db, err := buntdb.Open(goslConfig.dbNamePath)
  398. checkErrPanic(err)
  399. defer db.Close()
  400. var data string
  401. err = db.View(func(tx *buntdb.Tx) error {
  402. data, err = tx.Get(avatarName)
  403. return err
  404. })
  405. err = json.Unmarshal([]byte(data), &val)
  406. if err != nil {
  407. log.Errorf("Error while unparsing UUID for name: '%s' (%v)\n", avatarName, err)
  408. }
  409. } else if *goslConfig.database == "leveldb" {
  410. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  411. checkErrPanic(err)
  412. defer db.Close()
  413. data, err := db.Get([]byte(avatarName), nil)
  414. if err != nil {
  415. log.Errorf("Error while getting UUID for name: '%s' (%v)\n", avatarName, err)
  416. } else {
  417. err = json.Unmarshal(data, &val)
  418. if err != nil {
  419. log.Errorf("Error while unparsing UUID for name: '%s' (%v)\n", avatarName, err)
  420. }
  421. }
  422. }
  423. log.Debugf("Time to lookup '%s': %v\n", avatarName, time.Since(time_start))
  424. if err != nil {
  425. return NullUUID, ""
  426. } // else:
  427. return val.UUID, val.Grid
  428. }
  429. // searchKVUUID searches the KV database for an avatar key.
  430. func searchKVUUID(avatarKey string) (name string, grid string) {
  431. time_start := time.Now()
  432. checks := 0
  433. var val = avatarUUID{ NullUUID, "" }
  434. var found string
  435. if *goslConfig.database == "badger" {
  436. kv, err := badger.Open(Opt)
  437. checkErr(err) // should probably panic
  438. itOpt := badger.DefaultIteratorOptions
  439. /*
  440. if !*goslConfig.noMemory {
  441. itOpt.PrefetchValues = true
  442. itOpt.PrefetchSize = 1000 // attempt to get this a little bit more efficient; we have many small entries, so this is not too much
  443. } else {
  444. */
  445. itOpt.PrefetchValues = false // allegedly this is supposed to be WAY faster...
  446. // }
  447. txn := kv.NewTransaction(true)
  448. defer txn.Discard()
  449. err = kv.View(func(txn *badger.Txn) error {
  450. itr := txn.NewIterator(itOpt)
  451. defer itr.Close()
  452. for itr.Rewind(); itr.Valid(); itr.Next() {
  453. item := itr.Item()
  454. data, err := item.Value()
  455. if err != nil {
  456. log.Errorf("Error '%s' while getting data from %v\n", err, item)
  457. return err
  458. }
  459. err = json.Unmarshal(data, &val)
  460. if err != nil {
  461. log.Errorf("Error '%s' while unparsing UUID for data: %v\n", err, data)
  462. return err
  463. }
  464. checks++ //Just to see how many checks we made, for statistical purposes
  465. if avatarKey == val.UUID {
  466. found = string(item.Key())
  467. break
  468. }
  469. }
  470. return nil
  471. })
  472. kv.Close()
  473. } else if *goslConfig.database == "buntdb" {
  474. db, err := buntdb.Open(goslConfig.dbNamePath)
  475. checkErrPanic(err)
  476. err = db.View(func(tx *buntdb.Tx) error {
  477. err := tx.Ascend("", func(key, value string) bool {
  478. err = json.Unmarshal([]byte(value), &val)
  479. if err != nil {
  480. log.Errorf("Error '%s' while unparsing UUID for value: %v\n", err, value)
  481. }
  482. checks++ //Just to see how many checks we made, for statistical purposes
  483. if avatarKey == val.UUID {
  484. found = key
  485. return false
  486. }
  487. return true
  488. })
  489. return err
  490. })
  491. db.Close()
  492. } else if *goslConfig.database == "leveldb" {
  493. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  494. checkErrPanic(err)
  495. iter := db.NewIterator(nil, nil)
  496. for iter.Next() {
  497. // Remember that the contents of the returned slice should not be modified, and
  498. // only valid until the next call to Next.
  499. key := iter.Key()
  500. value := iter.Value()
  501. err = json.Unmarshal(value, &val)
  502. if err != nil {
  503. log.Errorf("Error '%s' while unparsing UUID for data: %v\n", err, value)
  504. continue // a bit insane, but at least we will skip a few broken records
  505. }
  506. checks++ //Just to see how many checks we made, for statistical purposes
  507. if avatarKey == val.UUID {
  508. found = string(key)
  509. break
  510. }
  511. }
  512. iter.Release()
  513. err = iter.Error()
  514. db.Close()
  515. }
  516. log.Debugf("Made %d checks for '%s' in %v\n", checks, avatarKey, time.Since(time_start))
  517. return found, val.Grid
  518. }
  519. // importDatabase is essentially reading a bzip2'ed CSV file with UUID,AvatarName downloaded from http://w-hat.com/#name2key .
  520. // One could theoretically set a cron job to get this file, save it on disk periodically, and keep the database up-to-date
  521. // see https://stackoverflow.com/questions/24673335/how-do-i-read-a-gzipped-csv-file for the actual usage of these complicated things!
  522. func importDatabase(filename string) {
  523. filehandler, err := os.Open(filename)
  524. if err != nil {
  525. log.Fatal(err)
  526. }
  527. defer filehandler.Close()
  528. gr := bzip2.NewReader(filehandler) // open bzip2 reader
  529. cr := csv.NewReader(gr) // open csv reader and feed the bzip2 reader into it
  530. limit := 0 // outside of for loop so that we can count how many entries we had in total
  531. time_start := time.Now() // we want to get an idea on how long this takes
  532. if *goslConfig.database == "badger" {
  533. // prepare connection to KV database
  534. kv, err := badger.Open(Opt)
  535. checkErrPanic(err) // should probably panic
  536. defer kv.Close()
  537. txn := kv.NewTransaction(true) // start new transaction; we will commit only every BATCH_BLOCK entries
  538. defer txn.Discard()
  539. for ;;limit++ {
  540. record, err := cr.Read()
  541. if err == io.EOF {
  542. break
  543. } else if err != nil {
  544. log.Fatal(err)
  545. }
  546. jsonNewEntry, err := json.Marshal(avatarUUID{ record[0], "Production" }) // W-Hat keys come all from the main LL grid, known as 'Production'
  547. if err != nil {
  548. log.Warning(err)
  549. } else {
  550. err = txn.Set([]byte(record[1]), jsonNewEntry)
  551. if err != nil {
  552. log.Fatal(err)
  553. }
  554. }
  555. if limit % goslConfig.BATCH_BLOCK == 0 && limit != 0 { // we do not run on the first time, and then only every BATCH_BLOCK times
  556. log.Info("Processing:", limit)
  557. err = txn.Commit(nil)
  558. if err != nil {
  559. log.Fatal(err)
  560. }
  561. runtime.GC()
  562. txn = kv.NewTransaction(true) // start a new transaction
  563. defer txn.Discard()
  564. }
  565. }
  566. // commit last batch
  567. err = txn.Commit(nil)
  568. if err != nil {
  569. log.Fatal(err)
  570. }
  571. kv.PurgeOlderVersions()
  572. } else if *goslConfig.database == "buntdb" {
  573. db, err := buntdb.Open(goslConfig.dbNamePath)
  574. checkErrPanic(err)
  575. defer db.Close()
  576. txn, err := db.Begin(true)
  577. checkErrPanic(err)
  578. //defer txn.Commit()
  579. // very similar to Badger code...
  580. for ;;limit++ {
  581. record, err := cr.Read()
  582. if err == io.EOF {
  583. break
  584. } else if err != nil {
  585. log.Fatal(err)
  586. }
  587. jsonNewEntry, err := json.Marshal(avatarUUID{ record[0], "Production" })
  588. if err != nil {
  589. log.Warning(err)
  590. } else {
  591. _, _, err = txn.Set(record[1], string(jsonNewEntry), nil)
  592. if err != nil {
  593. log.Fatal(err)
  594. }
  595. }
  596. if limit % goslConfig.BATCH_BLOCK == 0 && limit != 0 { // we do not run on the first time, and then only every BATCH_BLOCK times
  597. log.Info("Processing:", limit)
  598. err = txn.Commit()
  599. if err != nil {
  600. log.Fatal(err)
  601. }
  602. runtime.GC()
  603. txn, err = db.Begin(true) // start a new transaction
  604. checkErrPanic(err)
  605. //defer txn.Commit()
  606. }
  607. }
  608. // commit last batch
  609. err = txn.Commit()
  610. if err != nil {
  611. log.Fatal(err)
  612. }
  613. db.Shrink()
  614. } else if *goslConfig.database == "leveldb" {
  615. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  616. checkErrPanic(err)
  617. defer db.Close()
  618. batch := new(leveldb.Batch)
  619. for ;;limit++ {
  620. record, err := cr.Read()
  621. if err == io.EOF {
  622. break
  623. } else if err != nil {
  624. log.Fatal(err)
  625. }
  626. jsonNewEntry, err := json.Marshal(avatarUUID{ record[0], "Production" })
  627. if err != nil {
  628. log.Warning(err)
  629. } else {
  630. batch.Put([]byte(record[1]), jsonNewEntry)
  631. }
  632. if limit % goslConfig.BATCH_BLOCK == 0 && limit != 0 {
  633. log.Info("Processing:", limit)
  634. err = db.Write(batch, nil)
  635. if err != nil {
  636. log.Fatal(err)
  637. }
  638. batch.Reset() // unlike the others, we don't need to create a new batch every time
  639. runtime.GC() // it never hurts...
  640. }
  641. }
  642. // commit last batch
  643. err = db.Write(batch, nil)
  644. if err != nil {
  645. log.Fatal(err)
  646. }
  647. batch.Reset() // reset it and let the garbage collector run
  648. runtime.GC()
  649. db.CompactRange(util.Range{ nil, nil })
  650. }
  651. log.Info("Total read", limit, "records (or thereabouts) in", time.Since(time_start))
  652. }
  653. // NOTE(gwyneth): Auxiliary functions which I'm always using...
  654. // checkErrPanic logs a fatal error and panics.
  655. func checkErrPanic(err error) {
  656. if err != nil {
  657. pc, file, line, ok := runtime.Caller(1)
  658. log.Panic(filepath.Base(file), ":", line, ":", pc, ok, " - panic:", err)
  659. }
  660. }
  661. // checkErr checks if there is an error, and if yes, it logs it out and continues.
  662. // this is for 'normal' situations when we want to get a log if something goes wrong but do not need to panic
  663. func checkErr(err error) {
  664. if err != nil {
  665. pc, file, line, ok := runtime.Caller(1)
  666. log.Error(filepath.Base(file), ":", line, ":", pc, ok, " - error:", err)
  667. }
  668. }
  669. // Auxiliary functions for HTTP handling
  670. // checkErrHTTP returns an error via HTTP and also logs the error.
  671. func checkErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  672. if err != nil {
  673. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  674. pc, file, line, ok := runtime.Caller(1)
  675. log.Error("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - error:", errorMessage, err)
  676. }
  677. }
  678. // checkErrPanicHTTP returns an error via HTTP and logs the error with a panic.
  679. func checkErrPanicHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  680. if err != nil {
  681. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  682. pc, file, line, ok := runtime.Caller(1)
  683. log.Panic("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - panic:", errorMessage, err)
  684. }
  685. }
  686. // logErrHTTP assumes that the error message was already composed and writes it to HTTP and logs it.
  687. // this is mostly to avoid code duplication and make sure that all entries are written similarly
  688. func logErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string) {
  689. http.Error(w, errorMessage, httpStatus)
  690. log.Error("(" + http.StatusText(httpStatus) + ") " + errorMessage)
  691. }
  692. // funcName is @Sonia's solution to get the name of the function that Go is currently running.
  693. // This will be extensively used to deal with figuring out where in the code the errors are!
  694. // Source: https://stackoverflow.com/a/10743805/1035977 (20170708)
  695. func funcName() string {
  696. pc, _, _, _ := runtime.Caller(1)
  697. return runtime.FuncForPC(pc).Name()
  698. }
  699. // isValidUUID checks if the UUID is valid.
  700. // Thanks to Patrick D'Appollonio https://stackoverflow.com/questions/25051675/how-to-validate-uuid-v4-in-go
  701. func isValidUUID(uuid string) bool {
  702. r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
  703. return r.MatchString(uuid)
  704. }