2024-02-18 19:39:41 -05:00
package database
2020-05-04 16:25:57 +08:00
import (
"bufio"
2022-03-26 15:10:39 +08:00
"bytes"
2022-06-11 11:10:25 +08:00
"context"
2020-05-04 16:25:57 +08:00
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
2020-09-06 10:11:08 +08:00
"sync"
2020-05-04 16:25:57 +08:00
2026-01-22 08:20:53 -05:00
"github.com/cockroachdb/errors"
2020-05-04 16:25:57 +08:00
jsoniter "github.com/json-iterator/go"
2020-09-06 10:11:08 +08:00
"gorm.io/gorm"
"gorm.io/gorm/schema"
2020-05-04 16:25:57 +08:00
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/osutil"
)
// getTableType returns the type name of a table definition without package name,
2024-02-18 19:39:41 -05:00
// e.g. *database.LFSObject -> LFSObject.
2023-02-02 21:25:25 +08:00
func getTableType ( t any ) string {
2024-02-18 19:39:41 -05:00
return strings . TrimPrefix ( fmt . Sprintf ( "%T" , t ) , "*database." )
2020-05-04 16:25:57 +08:00
}
// DumpDatabase dumps all data from database to file system in JSON Lines format.
2022-06-11 11:10:25 +08:00
func DumpDatabase ( ctx context . Context , db * gorm . DB , dirPath string , verbose bool ) error {
2020-05-04 16:25:57 +08:00
err := os . MkdirAll ( dirPath , os . ModePerm )
if err != nil {
return err
}
2022-06-11 11:10:25 +08:00
err = dumpLegacyTables ( ctx , dirPath , verbose )
2020-05-04 16:25:57 +08:00
if err != nil {
return errors . Wrap ( err , "dump legacy tables" )
}
2020-09-06 17:02:25 +08:00
for _ , table := range Tables {
2022-06-11 11:10:25 +08:00
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
default :
}
2020-05-04 16:25:57 +08:00
tableName := getTableType ( table )
if verbose {
log . Trace ( "Dumping table %q..." , tableName )
}
err := func ( ) error {
tableFile := filepath . Join ( dirPath , tableName + ".json" )
f , err := os . Create ( tableFile )
if err != nil {
return errors . Wrap ( err , "create table file" )
}
defer func ( ) { _ = f . Close ( ) } ( )
2022-06-11 11:10:25 +08:00
return dumpTable ( ctx , db , table , f )
2020-05-04 16:25:57 +08:00
} ( )
if err != nil {
return errors . Wrapf ( err , "dump table %q" , tableName )
}
}
return nil
}
2023-02-02 21:25:25 +08:00
func dumpTable ( ctx context . Context , db * gorm . DB , table any , w io . Writer ) error {
2022-06-11 11:10:25 +08:00
query := db . WithContext ( ctx ) . Model ( table )
2020-05-04 16:25:57 +08:00
switch table . ( type ) {
case * LFSObject :
2022-06-11 11:10:25 +08:00
query = query . Order ( "repo_id, oid ASC" )
default :
query = query . Order ( "id ASC" )
2020-05-04 16:25:57 +08:00
}
rows , err := query . Rows ( )
if err != nil {
return errors . Wrap ( err , "select rows" )
}
defer func ( ) { _ = rows . Close ( ) } ( )
for rows . Next ( ) {
elem := reflect . New ( reflect . TypeOf ( table ) . Elem ( ) ) . Interface ( )
err = db . ScanRows ( rows , elem )
if err != nil {
return errors . Wrap ( err , "scan rows" )
}
2022-06-01 22:51:46 +08:00
switch e := elem . ( type ) {
case * LFSObject :
e . CreatedAt = e . CreatedAt . UTC ( )
}
2020-05-04 16:25:57 +08:00
err = jsoniter . NewEncoder ( w ) . Encode ( elem )
if err != nil {
return errors . Wrap ( err , "encode JSON" )
}
}
2021-11-10 08:29:27 +03:00
return rows . Err ( )
2020-05-04 16:25:57 +08:00
}
2022-06-11 11:10:25 +08:00
func dumpLegacyTables ( ctx context . Context , dirPath string , verbose bool ) error {
2020-05-04 16:25:57 +08:00
// Purposely create a local variable to not modify global variable
legacyTables := append ( legacyTables , new ( Version ) )
for _ , table := range legacyTables {
2022-06-11 11:10:25 +08:00
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
default :
}
2020-05-04 16:25:57 +08:00
tableName := getTableType ( table )
if verbose {
log . Trace ( "Dumping table %q..." , tableName )
}
2026-01-24 01:21:38 +00:00
err := func ( ) error {
tableFile := filepath . Join ( dirPath , tableName + ".json" )
f , err := os . Create ( tableFile )
if err != nil {
return errors . Wrap ( err , "create JSON file" )
}
defer func ( ) { _ = f . Close ( ) } ( )
2020-05-04 16:25:57 +08:00
2026-01-24 01:21:38 +00:00
return dumpTable ( ctx , db , table , f )
} ( )
if err != nil {
return errors . Wrapf ( err , "dump table %q" , tableName )
2020-05-04 16:25:57 +08:00
}
}
return nil
}
// ImportDatabase imports data from backup archive in JSON Lines format.
2022-06-11 11:10:25 +08:00
func ImportDatabase ( ctx context . Context , db * gorm . DB , dirPath string , verbose bool ) error {
err := importLegacyTables ( ctx , dirPath , verbose )
2020-05-04 16:25:57 +08:00
if err != nil {
return errors . Wrap ( err , "import legacy tables" )
}
2020-09-06 17:02:25 +08:00
for _ , table := range Tables {
2022-06-11 11:10:25 +08:00
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
default :
}
2024-02-18 19:39:41 -05:00
tableName := strings . TrimPrefix ( fmt . Sprintf ( "%T" , table ) , "*database." )
2020-05-04 16:25:57 +08:00
err := func ( ) error {
tableFile := filepath . Join ( dirPath , tableName + ".json" )
if ! osutil . IsFile ( tableFile ) {
log . Info ( "Skipped table %q" , tableName )
return nil
}
if verbose {
log . Trace ( "Importing table %q..." , tableName )
}
f , err := os . Open ( tableFile )
if err != nil {
return errors . Wrap ( err , "open table file" )
}
defer func ( ) { _ = f . Close ( ) } ( )
2022-06-11 11:10:25 +08:00
return importTable ( ctx , db , table , f )
2020-05-04 16:25:57 +08:00
} ( )
if err != nil {
return errors . Wrapf ( err , "import table %q" , tableName )
}
}
return nil
}
2023-02-02 21:25:25 +08:00
func importTable ( ctx context . Context , db * gorm . DB , table any , r io . Reader ) error {
2022-06-11 11:10:25 +08:00
err := db . WithContext ( ctx ) . Migrator ( ) . DropTable ( table )
2020-05-04 16:25:57 +08:00
if err != nil {
return errors . Wrap ( err , "drop table" )
}
2022-06-11 11:10:25 +08:00
err = db . WithContext ( ctx ) . Migrator ( ) . AutoMigrate ( table )
2020-05-04 16:25:57 +08:00
if err != nil {
return errors . Wrap ( err , "auto migrate" )
}
2020-09-06 10:11:08 +08:00
s , err := schema . Parse ( table , & sync . Map { } , db . NamingStrategy )
if err != nil {
return errors . Wrap ( err , "parse schema" )
}
rawTableName := s . Table
2020-05-04 16:25:57 +08:00
skipResetIDSeq := map [ string ] bool {
"lfs_object" : true ,
}
scanner := bufio . NewScanner ( r )
for scanner . Scan ( ) {
2022-03-26 15:10:39 +08:00
// PostgreSQL does not like the null characters (U+0000)
cleaned := bytes . ReplaceAll ( scanner . Bytes ( ) , [ ] byte ( "\\u0000" ) , [ ] byte ( "" ) )
2020-05-04 16:25:57 +08:00
elem := reflect . New ( reflect . TypeOf ( table ) . Elem ( ) ) . Interface ( )
2022-03-26 15:10:39 +08:00
err = jsoniter . Unmarshal ( cleaned , elem )
2020-05-04 16:25:57 +08:00
if err != nil {
return errors . Wrap ( err , "unmarshal JSON to struct" )
}
2022-06-11 11:10:25 +08:00
err = db . WithContext ( ctx ) . Create ( elem ) . Error
2020-05-04 16:25:57 +08:00
if err != nil {
return errors . Wrap ( err , "create row" )
}
}
// PostgreSQL needs manually reset table sequence for auto increment keys
if conf . UsePostgreSQL && ! skipResetIDSeq [ rawTableName ] {
seqName := rawTableName + "_id_seq"
2022-06-25 18:07:39 +08:00
if err = db . WithContext ( ctx ) . Exec ( fmt . Sprintf ( ` SELECT setval('%s', COALESCE((SELECT MAX(id)+1 FROM "%s"), 1), false) ` , seqName , rawTableName ) ) . Error ; err != nil {
2020-05-04 16:25:57 +08:00
return errors . Wrapf ( err , "reset table %q.%q" , rawTableName , seqName )
}
}
return nil
}
2022-06-11 11:10:25 +08:00
func importLegacyTables ( ctx context . Context , dirPath string , verbose bool ) error {
2020-05-04 16:25:57 +08:00
skipInsertProcessors := map [ string ] bool {
"mirror" : true ,
"milestone" : true ,
}
// Purposely create a local variable to not modify global variable
legacyTables := append ( legacyTables , new ( Version ) )
for _ , table := range legacyTables {
2022-06-11 11:10:25 +08:00
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
default :
}
2024-02-18 19:39:41 -05:00
tableName := strings . TrimPrefix ( fmt . Sprintf ( "%T" , table ) , "*database." )
2020-05-04 16:25:57 +08:00
tableFile := filepath . Join ( dirPath , tableName + ".json" )
if ! osutil . IsFile ( tableFile ) {
continue
}
if verbose {
log . Trace ( "Importing table %q..." , tableName )
}
2026-01-24 01:21:38 +00:00
if err := db . WithContext ( ctx ) . Migrator ( ) . DropTable ( table ) ; err != nil {
2026-01-22 08:20:53 -05:00
return errors . Newf ( "drop table %q: %v" , tableName , err )
2026-01-24 01:21:38 +00:00
} else if err = db . WithContext ( ctx ) . Migrator ( ) . AutoMigrate ( table ) ; err != nil {
2026-01-22 08:20:53 -05:00
return errors . Newf ( "sync table %q: %v" , tableName , err )
2020-05-04 16:25:57 +08:00
}
f , err := os . Open ( tableFile )
if err != nil {
2026-01-22 08:20:53 -05:00
return errors . Newf ( "open JSON file: %v" , err )
2020-05-04 16:25:57 +08:00
}
2026-01-24 01:21:38 +00:00
s , err := schema . Parse ( table , & sync . Map { } , db . NamingStrategy )
if err != nil {
_ = f . Close ( )
return errors . Wrap ( err , "parse schema" )
}
rawTableName := s . Table
_ , isInsertProcessor := table . ( interface { BeforeCreate ( * gorm . DB ) error } )
2020-05-04 16:25:57 +08:00
scanner := bufio . NewScanner ( f )
for scanner . Scan ( ) {
2026-01-24 01:21:38 +00:00
// PostgreSQL does not like the null characters (U+0000)
cleaned := bytes . ReplaceAll ( scanner . Bytes ( ) , [ ] byte ( "\\u0000" ) , [ ] byte ( "" ) )
if err = jsoniter . Unmarshal ( cleaned , table ) ; err != nil {
_ = f . Close ( )
2026-01-22 08:20:53 -05:00
return errors . Newf ( "unmarshal to struct: %v" , err )
2020-05-04 16:25:57 +08:00
}
2026-01-24 01:21:38 +00:00
if err = db . WithContext ( ctx ) . Create ( table ) . Error ; err != nil {
_ = f . Close ( )
return errors . Newf ( "insert struct: %v" , err )
2020-05-04 16:25:57 +08:00
}
2020-09-06 10:11:08 +08:00
var meta struct {
ID int64
CreatedUnix int64
DeadlineUnix int64
ClosedDateUnix int64
}
2026-01-24 01:21:38 +00:00
if err = jsoniter . Unmarshal ( cleaned , & meta ) ; err != nil {
2020-05-04 16:25:57 +08:00
log . Error ( "Failed to unmarshal to map: %v" , err )
}
2026-01-24 01:21:38 +00:00
// Reset created_unix back to the date saved in archive because Create method updates its value
2020-05-04 16:25:57 +08:00
if isInsertProcessor && ! skipInsertProcessors [ rawTableName ] {
2026-01-24 01:21:38 +00:00
if err = db . WithContext ( ctx ) . Exec ( "UPDATE `" + rawTableName + "` SET created_unix=? WHERE id=?" , meta . CreatedUnix , meta . ID ) . Error ; err != nil {
2020-09-06 10:11:08 +08:00
log . Error ( "Failed to reset '%s.created_unix': %v" , rawTableName , err )
2020-05-04 16:25:57 +08:00
}
}
switch rawTableName {
case "milestone" :
2026-01-24 01:21:38 +00:00
if err = db . WithContext ( ctx ) . Exec ( "UPDATE `" + rawTableName + "` SET deadline_unix=?, closed_date_unix=? WHERE id=?" , meta . DeadlineUnix , meta . ClosedDateUnix , meta . ID ) . Error ; err != nil {
2020-05-04 16:25:57 +08:00
log . Error ( "Failed to reset 'milestone.deadline_unix', 'milestone.closed_date_unix': %v" , err )
}
}
}
2026-01-24 01:21:38 +00:00
_ = f . Close ( )
2020-05-04 16:25:57 +08:00
// PostgreSQL needs manually reset table sequence for auto increment keys
if conf . UsePostgreSQL {
seqName := rawTableName + "_id_seq"
2026-01-24 01:21:38 +00:00
if err = db . WithContext ( ctx ) . Exec ( fmt . Sprintf ( ` SELECT setval('%s', COALESCE((SELECT MAX(id)+1 FROM "%s"), 1), false); ` , seqName , rawTableName ) ) . Error ; err != nil {
2026-01-22 08:20:53 -05:00
return errors . Newf ( "reset table %q' sequence: %v" , rawTableName , err )
2020-05-04 16:25:57 +08:00
}
}
}
return nil
}