mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-08-16 19:04:00 -05:00
Dumps store datetime columns as RFC3339 strings (e.g. 2026-03-27T11:27:01Z). On restore these were passed verbatim to the database, which MySQL and MariaDB reject with 'Error 1292 Incorrect datetime value'. Parse them into time.Time so the driver formats them for the target database. Also guard against a nil pointer dereference when a dump contains a column that no longer exists in the current schema - such columns are now dropped with a warning instead of crashing. Fixes https://github.com/go-vikunja/vikunja/issues/3375
199 lines
5.4 KiB
Go
199 lines
5.4 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package db
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/log"
|
|
|
|
"xorm.io/xorm/schemas"
|
|
)
|
|
|
|
var validTableName = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
|
|
|
// Formats a dumped time value may be in, depending on the database and driver the dump was created with.
|
|
var dumpTimeFormats = []string{
|
|
time.RFC3339Nano,
|
|
"2006-01-02 15:04:05.999999999Z07:00",
|
|
"2006-01-02 15:04:05.999999999",
|
|
"2006-01-02T15:04:05.999999999",
|
|
"2006-01-02",
|
|
}
|
|
|
|
// parseDumpTime parses a time string from a dump. Values without a timezone are
|
|
// interpreted as UTC since that's what dumps are written in.
|
|
func parseDumpTime(value string) (t time.Time, err error) {
|
|
for _, format := range dumpTimeFormats {
|
|
t, err = time.ParseInLocation(format, value, time.UTC)
|
|
if err == nil {
|
|
return t, nil
|
|
}
|
|
}
|
|
return t, err
|
|
}
|
|
|
|
func validateTableName(table string) error {
|
|
if !validTableName.MatchString(table) {
|
|
return fmt.Errorf("invalid table name: %q", table)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Dump dumps all Vikunja database tables
|
|
func Dump() (data map[string][]byte, err error) {
|
|
tableNames := RegisteredTableNames()
|
|
|
|
data = make(map[string][]byte, len(tableNames))
|
|
for _, name := range tableNames {
|
|
entries := []map[string]interface{}{}
|
|
err := x.Table(name).Find(&entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data[name], err = json.Marshal(entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Restore restores a table with all its entries
|
|
func Restore(table string, contents []map[string]interface{}) (err error) {
|
|
if err := validateTableName(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := x.IsTableExist(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
meta, err := x.DBMetas()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var metaForCurrentTable *schemas.Table
|
|
for _, m := range meta {
|
|
if m.Name == table {
|
|
metaForCurrentTable = m
|
|
break
|
|
}
|
|
}
|
|
|
|
if metaForCurrentTable == nil {
|
|
log.Fatalf("Could not find table definition for table %s", table)
|
|
}
|
|
|
|
for _, content := range contents {
|
|
for colName, value := range content {
|
|
col := metaForCurrentTable.GetColumn(colName)
|
|
if col == nil {
|
|
log.Warningf("Column %s does not exist in table %s, dropping it from the restored data", colName, table)
|
|
delete(content, colName)
|
|
continue
|
|
}
|
|
|
|
strVal, is := value.(string)
|
|
if !is || !col.SQLType.IsTime() {
|
|
continue
|
|
}
|
|
|
|
// Date fields might get restored as 0001-01-01 from null dates. This can have unintended side-effects like
|
|
// users being scheduled for deletion after a restore.
|
|
// To avoid this, we set these dates to nil so that they'll end up as null in the db.
|
|
if strVal == "" || strings.HasPrefix(strVal, "0001-") {
|
|
content[colName] = nil
|
|
continue
|
|
}
|
|
|
|
// Dumps contain dates as RFC3339 strings, which MySQL and MariaDB reject ("Incorrect
|
|
// datetime value"). Convert to time.Time and let the driver format it for the target db.
|
|
t, err := parseDumpTime(strVal)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse time value %q for column %s in table %s: %w", strVal, colName, table, err)
|
|
}
|
|
content[colName] = t
|
|
}
|
|
|
|
if _, err := x.Table(table).Insert(content); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if Type() == schemas.POSTGRES {
|
|
idSequence := table + "_id_seq"
|
|
_, err = x.Query(`SELECT setval('"` + idSequence + `"', COALESCE((SELECT MAX(id) FROM "` + table + `"), 1))`)
|
|
if err != nil {
|
|
log.Warningf("Could not reset id sequence for %s: %s", idSequence, err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// RestoreAndTruncate removes all content from the table before restoring it from the contents map
|
|
func RestoreAndTruncate(table string, contents []map[string]interface{}) (err error) {
|
|
if err := validateTableName(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := x.IsTableExist(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
if x.Dialect().URI().DBType == schemas.SQLITE {
|
|
if _, err := x.Query(`DELETE FROM "` + table + `"`); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(table)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return Restore(table, contents)
|
|
}
|
|
|
|
// TruncateAllTables deletes all data from every registered Vikunja table.
|
|
// Used by e2e tests to ensure a clean database state before each test.
|
|
func TruncateAllTables() error {
|
|
for _, name := range RegisteredTableNames() {
|
|
if err := validateTableName(name); err != nil {
|
|
return err
|
|
}
|
|
|
|
if x.Dialect().URI().DBType == schemas.SQLITE {
|
|
if _, err := x.Query(`DELETE FROM "` + name + `"`); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|