|
@@ -8,9 +8,11 @@ import (
|
8
|
8
|
"encoding/base64"
|
9
|
9
|
"encoding/json"
|
10
|
10
|
"fmt"
|
|
11
|
+ "io"
|
11
|
12
|
"log"
|
12
|
13
|
"os"
|
13
|
14
|
"strings"
|
|
15
|
+ "time"
|
14
|
16
|
|
15
|
17
|
"github.com/oragono/oragono/irc/modes"
|
16
|
18
|
"github.com/oragono/oragono/irc/passwd"
|
|
@@ -38,6 +40,22 @@ type SchemaChange struct {
|
38
|
40
|
// maps an initial version to a schema change capable of upgrading it
|
39
|
41
|
var schemaChanges map[string]SchemaChange
|
40
|
42
|
|
|
43
|
+type incompatibleSchemaError struct {
|
|
44
|
+ currentVersion string
|
|
45
|
+ requiredVersion string
|
|
46
|
+}
|
|
47
|
+
|
|
48
|
+func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
|
|
49
|
+ return &incompatibleSchemaError{
|
|
50
|
+ currentVersion: currentVersion,
|
|
51
|
+ requiredVersion: latestDbSchema,
|
|
52
|
+ }
|
|
53
|
+}
|
|
54
|
+
|
|
55
|
+func (err *incompatibleSchemaError) Error() string {
|
|
56
|
+ return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
|
|
57
|
+}
|
|
58
|
+
|
41
|
59
|
// InitDB creates the database.
|
42
|
60
|
func InitDB(path string) {
|
43
|
61
|
// prepare kvstore db
|
|
@@ -69,36 +87,107 @@ func InitDB(path string) {
|
69
|
87
|
}
|
70
|
88
|
|
71
|
89
|
// OpenDatabase returns an existing database, performing a schema version check.
|
72
|
|
-func OpenDatabase(path string) (*buntdb.DB, error) {
|
73
|
|
- // open data store
|
74
|
|
- db, err := buntdb.Open(path)
|
|
90
|
+func OpenDatabase(config *Config) (*buntdb.DB, error) {
|
|
91
|
+ allowAutoupgrade := true
|
|
92
|
+ if config.Datastore.AutoUpgrade != nil {
|
|
93
|
+ allowAutoupgrade = *config.Datastore.AutoUpgrade
|
|
94
|
+ }
|
|
95
|
+ return openDatabaseInternal(config, allowAutoupgrade)
|
|
96
|
+}
|
|
97
|
+
|
|
98
|
+// open the database, giving it at most one chance to auto-upgrade the schema
|
|
99
|
+func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
|
|
100
|
+ db, err = buntdb.Open(config.Datastore.Path)
|
75
|
101
|
if err != nil {
|
76
|
|
- return nil, err
|
|
102
|
+ return
|
77
|
103
|
}
|
78
|
104
|
|
79
|
|
- // check db version
|
80
|
|
- err = db.View(func(tx *buntdb.Tx) error {
|
81
|
|
- version, _ := tx.Get(keySchemaVersion)
|
82
|
|
- if version != latestDbSchema {
|
83
|
|
- return fmt.Errorf("Database must be updated. Expected schema v%s, got v%s", latestDbSchema, version)
|
|
105
|
+ defer func() {
|
|
106
|
+ if err != nil && db != nil {
|
|
107
|
+ db.Close()
|
|
108
|
+ db = nil
|
84
|
109
|
}
|
85
|
|
- return nil
|
|
110
|
+ }()
|
|
111
|
+
|
|
112
|
+ // read the current version string
|
|
113
|
+ var version string
|
|
114
|
+ err = db.View(func(tx *buntdb.Tx) error {
|
|
115
|
+ version, err = tx.Get(keySchemaVersion)
|
|
116
|
+ return err
|
86
|
117
|
})
|
|
118
|
+ if err != nil {
|
|
119
|
+ return
|
|
120
|
+ }
|
87
|
121
|
|
|
122
|
+ if version == latestDbSchema {
|
|
123
|
+ // success
|
|
124
|
+ return
|
|
125
|
+ }
|
|
126
|
+
|
|
127
|
+ // XXX quiesce the DB so we can be sure it's safe to make a backup copy
|
|
128
|
+ db.Close()
|
|
129
|
+ db = nil
|
|
130
|
+ if allowAutoupgrade {
|
|
131
|
+ err = performAutoUpgrade(version, config)
|
|
132
|
+ if err != nil {
|
|
133
|
+ return
|
|
134
|
+ }
|
|
135
|
+ // successful autoupgrade, let's try this again:
|
|
136
|
+ return openDatabaseInternal(config, false)
|
|
137
|
+ } else {
|
|
138
|
+ err = IncompatibleSchemaError(version)
|
|
139
|
+ return
|
|
140
|
+ }
|
|
141
|
+}
|
|
142
|
+
|
|
143
|
+// implementation of `cp` (go should really provide this...)
|
|
144
|
+func cpFile(src string, dst string) (err error) {
|
|
145
|
+ in, err := os.Open(src)
|
|
146
|
+ if err != nil {
|
|
147
|
+ return
|
|
148
|
+ }
|
|
149
|
+ defer in.Close()
|
|
150
|
+ out, err := os.Create(dst)
|
88
|
151
|
if err != nil {
|
89
|
|
- // close the db
|
90
|
|
- db.Close()
|
91
|
|
- return nil, err
|
|
152
|
+ return
|
|
153
|
+ }
|
|
154
|
+ defer func() {
|
|
155
|
+ closeError := out.Close()
|
|
156
|
+ if err == nil {
|
|
157
|
+ err = closeError
|
|
158
|
+ }
|
|
159
|
+ }()
|
|
160
|
+ if _, err = io.Copy(out, in); err != nil {
|
|
161
|
+ return
|
92
|
162
|
}
|
|
163
|
+ return
|
|
164
|
+}
|
93
|
165
|
|
94
|
|
- return db, nil
|
|
166
|
+func performAutoUpgrade(currentVersion string, config *Config) (err error) {
|
|
167
|
+ path := config.Datastore.Path
|
|
168
|
+ log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema)
|
|
169
|
+ timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
|
|
170
|
+ backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp)
|
|
171
|
+ log.Printf("making a backup of current database at %s\n", backupPath)
|
|
172
|
+ err = cpFile(path, backupPath)
|
|
173
|
+ if err != nil {
|
|
174
|
+ return err
|
|
175
|
+ }
|
|
176
|
+
|
|
177
|
+ err = UpgradeDB(config)
|
|
178
|
+ if err != nil {
|
|
179
|
+ // database upgrade is a single transaction, so we don't need to restore the backup;
|
|
180
|
+ // we can just delete it
|
|
181
|
+ os.Remove(backupPath)
|
|
182
|
+ }
|
|
183
|
+ return err
|
95
|
184
|
}
|
96
|
185
|
|
97
|
186
|
// UpgradeDB upgrades the datastore to the latest schema.
|
98
|
|
-func UpgradeDB(config *Config) {
|
|
187
|
+func UpgradeDB(config *Config) (err error) {
|
99
|
188
|
store, err := buntdb.Open(config.Datastore.Path)
|
100
|
189
|
if err != nil {
|
101
|
|
- log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error()))
|
|
190
|
+ return err
|
102
|
191
|
}
|
103
|
192
|
defer store.Close()
|
104
|
193
|
|
|
@@ -108,9 +197,14 @@ func UpgradeDB(config *Config) {
|
108
|
197
|
version, _ = tx.Get(keySchemaVersion)
|
109
|
198
|
change, schemaNeedsChange := schemaChanges[version]
|
110
|
199
|
if !schemaNeedsChange {
|
111
|
|
- break
|
|
200
|
+ if version == latestDbSchema {
|
|
201
|
+ // success!
|
|
202
|
+ break
|
|
203
|
+ }
|
|
204
|
+ // unable to upgrade to the desired version, roll back
|
|
205
|
+ return IncompatibleSchemaError(version)
|
112
|
206
|
}
|
113
|
|
- log.Println("attempting to update store from version " + version)
|
|
207
|
+ log.Println("attempting to update schema from version " + version)
|
114
|
208
|
err := change.Changer(config, tx)
|
115
|
209
|
if err != nil {
|
116
|
210
|
return err
|
|
@@ -119,16 +213,15 @@ func UpgradeDB(config *Config) {
|
119
|
213
|
if err != nil {
|
120
|
214
|
return err
|
121
|
215
|
}
|
122
|
|
- log.Println("successfully updated store to version " + change.TargetVersion)
|
|
216
|
+ log.Println("successfully updated schema to version " + change.TargetVersion)
|
123
|
217
|
}
|
124
|
218
|
return nil
|
125
|
219
|
})
|
126
|
220
|
|
127
|
221
|
if err != nil {
|
128
|
|
- log.Fatal("Could not update datastore:", err.Error())
|
|
222
|
+ log.Println("database upgrade failed and was rolled back")
|
129
|
223
|
}
|
130
|
|
-
|
131
|
|
- return
|
|
224
|
+ return err
|
132
|
225
|
}
|
133
|
226
|
|
134
|
227
|
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|