You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

database.go 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. // Copyright (c) 2012-2014 Jeremy Latt
  2. // Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
  3. // released under the MIT license
  4. package irc
  5. import (
  6. "encoding/json"
  7. "fmt"
  8. "log"
  9. "os"
  10. "strings"
  11. "time"
  12. "github.com/oragono/oragono/irc/modes"
  13. "github.com/oragono/oragono/irc/utils"
  14. "github.com/tidwall/buntdb"
  15. )
  16. const (
  17. // 'version' of the database schema
  18. keySchemaVersion = "db.version"
  19. // latest schema of the db
  20. latestDbSchema = "3"
  21. )
  22. type SchemaChanger func(*Config, *buntdb.Tx) error
  23. type SchemaChange struct {
  24. InitialVersion string // the change will take this version
  25. TargetVersion string // and transform it into this version
  26. Changer SchemaChanger
  27. }
  28. // maps an initial version to a schema change capable of upgrading it
  29. var schemaChanges map[string]SchemaChange
  30. type incompatibleSchemaError struct {
  31. currentVersion string
  32. requiredVersion string
  33. }
  34. func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
  35. return &incompatibleSchemaError{
  36. currentVersion: currentVersion,
  37. requiredVersion: latestDbSchema,
  38. }
  39. }
  40. func (err *incompatibleSchemaError) Error() string {
  41. return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
  42. }
  43. // InitDB creates the database.
  44. func InitDB(path string) {
  45. // prepare kvstore db
  46. //TODO(dan): fail if already exists instead? don't want to overwrite good data
  47. os.Remove(path)
  48. store, err := buntdb.Open(path)
  49. if err != nil {
  50. log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error()))
  51. }
  52. defer store.Close()
  53. err = store.Update(func(tx *buntdb.Tx) error {
  54. // set schema version
  55. tx.Set(keySchemaVersion, latestDbSchema, nil)
  56. return nil
  57. })
  58. if err != nil {
  59. log.Fatal("Could not save datastore:", err.Error())
  60. }
  61. }
  62. // OpenDatabase returns an existing database, performing a schema version check.
  63. func OpenDatabase(config *Config) (*buntdb.DB, error) {
  64. return openDatabaseInternal(config, config.Datastore.AutoUpgrade)
  65. }
  66. // open the database, giving it at most one chance to auto-upgrade the schema
  67. func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
  68. _, err = os.Stat(config.Datastore.Path)
  69. if os.IsNotExist(err) {
  70. return
  71. }
  72. db, err = buntdb.Open(config.Datastore.Path)
  73. if err != nil {
  74. return
  75. }
  76. defer func() {
  77. if err != nil && db != nil {
  78. db.Close()
  79. db = nil
  80. }
  81. }()
  82. // read the current version string
  83. var version string
  84. err = db.View(func(tx *buntdb.Tx) error {
  85. version, err = tx.Get(keySchemaVersion)
  86. return err
  87. })
  88. if err != nil {
  89. return
  90. }
  91. if version == latestDbSchema {
  92. // success
  93. return
  94. }
  95. // XXX quiesce the DB so we can be sure it's safe to make a backup copy
  96. db.Close()
  97. db = nil
  98. if allowAutoupgrade {
  99. err = performAutoUpgrade(version, config)
  100. if err != nil {
  101. return
  102. }
  103. // successful autoupgrade, let's try this again:
  104. return openDatabaseInternal(config, false)
  105. } else {
  106. err = IncompatibleSchemaError(version)
  107. return
  108. }
  109. }
  110. func performAutoUpgrade(currentVersion string, config *Config) (err error) {
  111. path := config.Datastore.Path
  112. log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema)
  113. timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
  114. backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp)
  115. log.Printf("making a backup of current database at %s\n", backupPath)
  116. err = utils.CopyFile(path, backupPath)
  117. if err != nil {
  118. return err
  119. }
  120. err = UpgradeDB(config)
  121. if err != nil {
  122. // database upgrade is a single transaction, so we don't need to restore the backup;
  123. // we can just delete it
  124. os.Remove(backupPath)
  125. }
  126. return err
  127. }
  128. // UpgradeDB upgrades the datastore to the latest schema.
  129. func UpgradeDB(config *Config) (err error) {
  130. store, err := buntdb.Open(config.Datastore.Path)
  131. if err != nil {
  132. return err
  133. }
  134. defer store.Close()
  135. var version string
  136. err = store.Update(func(tx *buntdb.Tx) error {
  137. for {
  138. version, _ = tx.Get(keySchemaVersion)
  139. change, schemaNeedsChange := schemaChanges[version]
  140. if !schemaNeedsChange {
  141. if version == latestDbSchema {
  142. // success!
  143. break
  144. }
  145. // unable to upgrade to the desired version, roll back
  146. return IncompatibleSchemaError(version)
  147. }
  148. log.Println("attempting to update schema from version " + version)
  149. err := change.Changer(config, tx)
  150. if err != nil {
  151. return err
  152. }
  153. _, _, err = tx.Set(keySchemaVersion, change.TargetVersion, nil)
  154. if err != nil {
  155. return err
  156. }
  157. log.Println("successfully updated schema to version " + change.TargetVersion)
  158. }
  159. return nil
  160. })
  161. if err != nil {
  162. log.Println("database upgrade failed and was rolled back")
  163. }
  164. return err
  165. }
  166. func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
  167. // == version 1 -> 2 ==
  168. // account key changes and account.verified key bugfix.
  169. var keysToRemove []string
  170. newKeys := make(map[string]string)
  171. tx.AscendKeys("account *", func(key, value string) bool {
  172. keysToRemove = append(keysToRemove, key)
  173. splitkey := strings.Split(key, " ")
  174. // work around bug
  175. if splitkey[2] == "exists" {
  176. // manually create new verified key
  177. newVerifiedKey := fmt.Sprintf("%s.verified %s", splitkey[0], splitkey[1])
  178. newKeys[newVerifiedKey] = "1"
  179. } else if splitkey[1] == "%s" {
  180. return true
  181. }
  182. newKey := fmt.Sprintf("%s.%s %s", splitkey[0], splitkey[2], splitkey[1])
  183. newKeys[newKey] = value
  184. return true
  185. })
  186. for _, key := range keysToRemove {
  187. tx.Delete(key)
  188. }
  189. for key, value := range newKeys {
  190. tx.Set(key, value, nil)
  191. }
  192. return nil
  193. }
  194. // 1. channel founder names should be casefolded
  195. // 2. founder should be explicitly granted the ChannelFounder user mode
  196. // 3. explicitly initialize stored channel modes to the server default values
  197. func schemaChangeV2ToV3(config *Config, tx *buntdb.Tx) error {
  198. var channels []string
  199. prefix := "channel.exists "
  200. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  201. if !strings.HasPrefix(key, prefix) {
  202. return false
  203. }
  204. chname := strings.TrimPrefix(key, prefix)
  205. channels = append(channels, chname)
  206. return true
  207. })
  208. // founder names should be casefolded
  209. // founder should be explicitly granted the ChannelFounder user mode
  210. for _, channel := range channels {
  211. founderKey := "channel.founder " + channel
  212. founder, _ := tx.Get(founderKey)
  213. if founder != "" {
  214. founder, err := CasefoldName(founder)
  215. if err == nil {
  216. tx.Set(founderKey, founder, nil)
  217. accountToUmode := map[string]modes.Mode{
  218. founder: modes.ChannelFounder,
  219. }
  220. atustr, _ := json.Marshal(accountToUmode)
  221. tx.Set("channel.accounttoumode "+channel, string(atustr), nil)
  222. }
  223. }
  224. }
  225. // explicitly store the channel modes
  226. defaultModes := config.Channels.defaultModes
  227. modeStrings := make([]string, len(defaultModes))
  228. for i, mode := range defaultModes {
  229. modeStrings[i] = string(mode)
  230. }
  231. defaultModeString := strings.Join(modeStrings, "")
  232. for _, channel := range channels {
  233. tx.Set("channel.modes "+channel, defaultModeString, nil)
  234. }
  235. return nil
  236. }
  237. func init() {
  238. allChanges := []SchemaChange{
  239. {
  240. InitialVersion: "1",
  241. TargetVersion: "2",
  242. Changer: schemaChangeV1toV2,
  243. },
  244. {
  245. InitialVersion: "2",
  246. TargetVersion: "3",
  247. Changer: schemaChangeV2ToV3,
  248. },
  249. }
  250. // build the index
  251. schemaChanges = make(map[string]SchemaChange)
  252. for _, change := range allChanges {
  253. schemaChanges[change.InitialVersion] = change
  254. }
  255. }