2014-05-02 05:21:46 +04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2016-12-21 15:13:17 +03:00
// Copyright 2016 The Gitea Authors. All rights reserved.
2014-05-02 05:21:46 +04:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
2014-05-05 21:08:01 +04:00
"fmt"
2014-05-02 05:21:46 +04:00
"os"
"path"
2017-01-12 07:47:20 +03:00
"path/filepath"
2020-06-05 23:47:39 +03:00
"strings"
2014-05-05 21:08:01 +04:00
"time"
2014-05-02 05:21:46 +04:00
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2021-07-24 19:03:58 +03:00
"code.gitea.io/gitea/modules/json"
2019-12-17 19:12:10 +03:00
"code.gitea.io/gitea/modules/log"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/setting"
2020-09-29 12:05:13 +03:00
"code.gitea.io/gitea/modules/storage"
2020-08-11 23:05:34 +03:00
"code.gitea.io/gitea/modules/util"
2017-04-12 10:44:54 +03:00
2021-01-26 18:36:53 +03:00
"gitea.com/go-chi/session"
2020-06-05 23:47:39 +03:00
archiver "github.com/mholt/archiver/v3"
2016-11-10 01:18:22 +03:00
"github.com/urfave/cli"
2014-05-02 05:21:46 +04:00
)
2020-06-05 23:47:39 +03:00
func addFile ( w archiver . Writer , filePath string , absPath string , verbose bool ) error {
if verbose {
log . Info ( "Adding file %s\n" , filePath )
}
file , err := os . Open ( absPath )
if err != nil {
return err
}
defer file . Close ( )
fileInfo , err := file . Stat ( )
if err != nil {
return err
}
return w . Write ( archiver . File {
FileInfo : archiver . FileInfo {
FileInfo : fileInfo ,
CustomName : filePath ,
} ,
ReadCloser : file ,
} )
}
func isSubdir ( upper string , lower string ) ( bool , error ) {
if relPath , err := filepath . Rel ( upper , lower ) ; err != nil {
return false , err
} else if relPath == "." || ! strings . HasPrefix ( relPath , "." ) {
return true , nil
}
return false , nil
}
type outputType struct {
Enum [ ] string
Default string
selected string
}
func ( o outputType ) Join ( ) string {
return strings . Join ( o . Enum , ", " )
}
func ( o * outputType ) Set ( value string ) error {
for _ , enum := range o . Enum {
if enum == value {
o . selected = value
return nil
}
}
return fmt . Errorf ( "allowed values are %s" , o . Join ( ) )
}
func ( o outputType ) String ( ) string {
if o . selected == "" {
return o . Default
}
return o . selected
}
var outputTypeEnum = & outputType {
2021-12-17 16:38:45 +03:00
Enum : [ ] string { "zip" , "rar" , "tar" , "sz" , "tar.gz" , "tar.xz" , "tar.bz2" , "tar.br" , "tar.lz4" } ,
2020-06-05 23:47:39 +03:00
Default : "zip" ,
}
2016-11-04 14:42:18 +03:00
// CmdDump represents the available dump sub-command.
2014-05-02 05:21:46 +04:00
var CmdDump = cli . Command {
Name : "dump" ,
2016-12-21 15:13:17 +03:00
Usage : "Dump Gitea files and database" ,
2014-05-05 08:55:17 +04:00
Description : ` Dump compresses all related files and database into zip file .
2016-12-21 15:13:17 +03:00
It can be used for backup and capture Gitea server image to send to maintainer ` ,
2014-05-02 05:21:46 +04:00
Action : runDump ,
2014-09-08 03:39:26 +04:00
Flags : [ ] cli . Flag {
2019-04-01 07:31:37 +03:00
cli . StringFlag {
Name : "file, f" ,
Value : fmt . Sprintf ( "gitea-dump-%d.zip" , time . Now ( ) . Unix ( ) ) ,
2020-06-05 23:47:39 +03:00
Usage : "Name of the dump file which will be created. Supply '-' for stdout. See type for available types." ,
2019-04-01 07:31:37 +03:00
} ,
2016-11-10 01:18:22 +03:00
cli . BoolFlag {
2019-05-01 23:36:09 +03:00
Name : "verbose, V" ,
2016-11-10 01:18:22 +03:00
Usage : "Show process details" ,
} ,
cli . StringFlag {
Name : "tempdir, t" ,
Value : os . TempDir ( ) ,
Usage : "Temporary dir path" ,
} ,
2017-01-03 11:20:28 +03:00
cli . StringFlag {
Name : "database, d" ,
Usage : "Specify the database SQL syntax" ,
} ,
2019-01-14 00:52:26 +03:00
cli . BoolFlag {
Name : "skip-repository, R" ,
Usage : "Skip the repository dumping" ,
} ,
2020-05-01 04:30:31 +03:00
cli . BoolFlag {
Name : "skip-log, L" ,
Usage : "Skip the log dumping" ,
} ,
2021-02-08 04:00:12 +03:00
cli . BoolFlag {
Name : "skip-custom-dir" ,
Usage : "Skip custom directory" ,
} ,
2021-04-12 12:33:32 +03:00
cli . BoolFlag {
Name : "skip-lfs-data" ,
Usage : "Skip LFS data" ,
} ,
cli . BoolFlag {
Name : "skip-attachment-data" ,
Usage : "Skip attachment data" ,
} ,
2020-06-05 23:47:39 +03:00
cli . GenericFlag {
Name : "type" ,
Value : outputTypeEnum ,
Usage : fmt . Sprintf ( "Dump output format: %s" , outputTypeEnum . Join ( ) ) ,
} ,
2014-09-08 03:39:26 +04:00
} ,
2014-05-02 05:21:46 +04:00
}
2019-12-17 19:12:10 +03:00
func fatal ( format string , args ... interface { } ) {
fmt . Fprintf ( os . Stderr , format + "\n" , args ... )
log . Fatal ( format , args ... )
}
2016-05-12 21:32:28 +03:00
func runDump ( ctx * cli . Context ) error {
2020-06-05 23:47:39 +03:00
var file * os . File
fileName := ctx . String ( "file" )
2021-12-17 16:38:45 +03:00
outType := ctx . String ( "type" )
2020-06-05 23:47:39 +03:00
if fileName == "-" {
file = os . Stdout
err := log . DelLogger ( "console" )
if err != nil {
fatal ( "Deleting default logger failed. Can not write to stdout: %v" , err )
}
2021-12-17 16:38:45 +03:00
} else {
fileName = strings . TrimSuffix ( fileName , path . Ext ( fileName ) )
fileName += "." + outType
2020-06-05 23:47:39 +03:00
}
2021-12-01 10:50:01 +03:00
setting . LoadFromExisting ( )
2020-06-05 23:47:39 +03:00
// make sure we are logging to the console no matter what the configuration tells us do to
if _ , err := setting . Cfg . Section ( "log" ) . NewKey ( "MODE" , "console" ) ; err != nil {
fatal ( "Setting logging mode to console failed: %v" , err )
}
if _ , err := setting . Cfg . Section ( "log.console" ) . NewKey ( "STDERR" , "true" ) ; err != nil {
fatal ( "Setting console logger to stderr failed: %v" , err )
}
2020-09-08 01:27:17 +03:00
if ! setting . InstallLock {
log . Error ( "Is '%s' really the right config path?\n" , setting . CustomConf )
return fmt . Errorf ( "gitea is not initialized" )
}
2017-01-12 07:47:20 +03:00
setting . NewServices ( ) // cannot access session settings otherwise
2017-01-23 12:11:18 +03:00
2021-11-07 06:11:27 +03:00
stdCtx , cancel := installSignals ( )
defer cancel ( )
err := db . InitEngine ( stdCtx )
2017-01-23 12:11:18 +03:00
if err != nil {
return err
}
2014-05-02 05:21:46 +04:00
2020-09-29 12:05:13 +03:00
if err := storage . Init ( ) ; err != nil {
return err
}
2020-06-05 23:47:39 +03:00
if file == nil {
file , err = os . Create ( fileName )
if err != nil {
fatal ( "Unable to open %s: %v" , fileName , err )
}
2015-11-28 16:07:51 +03:00
}
2020-06-05 23:47:39 +03:00
defer file . Close ( )
2015-11-28 16:07:51 +03:00
2021-02-08 04:00:12 +03:00
absFileName , err := filepath . Abs ( fileName )
if err != nil {
return err
}
2020-06-05 23:47:39 +03:00
verbose := ctx . Bool ( "verbose" )
var iface interface { }
if fileName == "-" {
iface , err = archiver . ByExtension ( fmt . Sprintf ( ".%s" , outType ) )
} else {
iface , err = archiver . ByExtension ( fileName )
2017-06-09 03:24:15 +03:00
}
2019-01-14 00:52:26 +03:00
if err != nil {
2020-06-05 23:47:39 +03:00
fatal ( "Unable to get archiver for extension: %v" , err )
2019-01-14 00:52:26 +03:00
}
2019-12-17 19:12:10 +03:00
2020-06-05 23:47:39 +03:00
w , _ := iface . ( archiver . Writer )
if err := w . Create ( file ) ; err != nil {
fatal ( "Creating archiver.Writer failed: %v" , err )
}
defer w . Close ( )
2019-01-14 00:52:26 +03:00
2020-05-03 06:57:45 +03:00
if ctx . IsSet ( "skip-repository" ) && ctx . Bool ( "skip-repository" ) {
2019-12-17 19:12:10 +03:00
log . Info ( "Skip dumping local repositories" )
2019-01-14 00:52:26 +03:00
} else {
2020-06-05 23:47:39 +03:00
log . Info ( "Dumping local repositories... %s" , setting . RepoRootPath )
2021-02-08 04:00:12 +03:00
if err := addRecursiveExclude ( w , "repos" , setting . RepoRootPath , [ ] string { absFileName } , verbose ) ; err != nil {
2020-06-05 23:47:39 +03:00
fatal ( "Failed to include repositories: %v" , err )
2019-01-14 00:52:26 +03:00
}
2020-06-05 23:47:39 +03:00
2021-04-12 12:33:32 +03:00
if ctx . IsSet ( "skip-lfs-data" ) && ctx . Bool ( "skip-lfs-data" ) {
log . Info ( "Skip dumping LFS data" )
} else if err := storage . LFS . IterateObjects ( func ( objPath string , object storage . Object ) error {
2020-09-29 12:05:13 +03:00
info , err := object . Stat ( )
if err != nil {
return err
2020-06-05 23:47:39 +03:00
}
2020-09-29 12:05:13 +03:00
return w . Write ( archiver . File {
FileInfo : archiver . FileInfo {
FileInfo : info ,
CustomName : path . Join ( "data" , "lfs" , objPath ) ,
} ,
ReadCloser : object ,
} )
} ) ; err != nil {
fatal ( "Failed to dump LFS objects: %v" , err )
2019-01-14 00:52:26 +03:00
}
2014-05-02 05:21:46 +04:00
}
2020-06-05 23:47:39 +03:00
tmpDir := ctx . String ( "tempdir" )
if _ , err := os . Stat ( tmpDir ) ; os . IsNotExist ( err ) {
fatal ( "Path does not exist: %s" , tmpDir )
}
2021-09-22 08:38:34 +03:00
dbDump , err := os . CreateTemp ( tmpDir , "gitea-db.sql" )
2020-06-05 23:47:39 +03:00
if err != nil {
fatal ( "Failed to create tmp file: %v" , err )
}
2020-08-11 23:05:34 +03:00
defer func ( ) {
if err := util . Remove ( dbDump . Name ( ) ) ; err != nil {
log . Warn ( "Unable to remove temporary file: %s: Error: %v" , dbDump . Name ( ) , err )
}
} ( )
2020-06-05 23:47:39 +03:00
2017-01-03 11:20:28 +03:00
targetDBType := ctx . String ( "database" )
2019-08-24 12:24:45 +03:00
if len ( targetDBType ) > 0 && targetDBType != setting . Database . Type {
2019-12-17 19:12:10 +03:00
log . Info ( "Dumping database %s => %s..." , setting . Database . Type , targetDBType )
2017-01-03 11:20:28 +03:00
} else {
2019-12-17 19:12:10 +03:00
log . Info ( "Dumping database..." )
2017-01-03 11:20:28 +03:00
}
2021-09-19 14:49:59 +03:00
if err := db . DumpDatabase ( dbDump . Name ( ) , targetDBType ) ; err != nil {
2019-12-17 19:12:10 +03:00
fatal ( "Failed to dump database: %v" , err )
2014-05-05 08:55:17 +04:00
}
2020-06-05 23:47:39 +03:00
if err := addFile ( w , "gitea-db.sql" , dbDump . Name ( ) , verbose ) ; err != nil {
2019-12-17 19:12:10 +03:00
fatal ( "Failed to include gitea-db.sql: %v" , err )
2015-11-28 14:11:38 +03:00
}
2019-04-05 16:24:28 +03:00
if len ( setting . CustomConf ) > 0 {
2019-12-17 19:12:10 +03:00
log . Info ( "Adding custom configuration file from %s" , setting . CustomConf )
2020-06-05 23:47:39 +03:00
if err := addFile ( w , "app.ini" , setting . CustomConf , verbose ) ; err != nil {
2019-12-17 19:12:10 +03:00
fatal ( "Failed to include specified app.ini: %v" , err )
2019-04-05 16:24:28 +03:00
}
}
2021-02-08 04:00:12 +03:00
if ctx . IsSet ( "skip-custom-dir" ) && ctx . Bool ( "skip-custom-dir" ) {
2021-07-08 14:38:13 +03:00
log . Info ( "Skipping custom directory" )
2021-02-08 04:00:12 +03:00
} else {
customDir , err := os . Stat ( setting . CustomPath )
if err == nil && customDir . IsDir ( ) {
if is , _ := isSubdir ( setting . AppDataPath , setting . CustomPath ) ; ! is {
if err := addRecursiveExclude ( w , "custom" , setting . CustomPath , [ ] string { absFileName } , verbose ) ; err != nil {
fatal ( "Failed to include custom: %v" , err )
}
} else {
log . Info ( "Custom dir %s is inside data dir %s, skipped" , setting . CustomPath , setting . AppDataPath )
2020-06-05 23:47:39 +03:00
}
} else {
2021-02-08 04:00:12 +03:00
log . Info ( "Custom dir %s doesn't exist, skipped" , setting . CustomPath )
2016-05-12 21:32:28 +03:00
}
2015-11-28 14:11:38 +03:00
}
2017-01-12 07:47:20 +03:00
2020-11-28 05:42:08 +03:00
isExist , err := util . IsExist ( setting . AppDataPath )
if err != nil {
log . Error ( "Unable to check if %s exists. Error: %v" , setting . AppDataPath , err )
}
if isExist {
2019-12-17 19:12:10 +03:00
log . Info ( "Packing data directory...%s" , setting . AppDataPath )
2017-01-12 07:47:20 +03:00
2020-06-05 23:47:39 +03:00
var excludes [ ] string
if setting . Cfg . Section ( "session" ) . Key ( "PROVIDER" ) . Value ( ) == "file" {
var opts session . Options
if err = json . Unmarshal ( [ ] byte ( setting . SessionConfig . ProviderConfig ) , & opts ) ; err != nil {
return err
}
excludes = append ( excludes , opts . ProviderConfig )
2017-03-02 12:41:33 +03:00
}
2020-06-05 23:47:39 +03:00
excludes = append ( excludes , setting . RepoRootPath )
2020-09-29 12:05:13 +03:00
excludes = append ( excludes , setting . LFS . Path )
excludes = append ( excludes , setting . Attachment . Path )
2020-06-05 23:47:39 +03:00
excludes = append ( excludes , setting . LogRootPath )
2021-02-08 04:00:12 +03:00
excludes = append ( excludes , absFileName )
2020-06-05 23:47:39 +03:00
if err := addRecursiveExclude ( w , "data" , setting . AppDataPath , excludes , verbose ) ; err != nil {
2019-12-17 19:12:10 +03:00
fatal ( "Failed to include data directory: %v" , err )
2017-03-02 12:41:33 +03:00
}
2017-01-12 07:47:20 +03:00
}
2021-04-12 12:33:32 +03:00
if ctx . IsSet ( "skip-attachment-data" ) && ctx . Bool ( "skip-attachment-data" ) {
log . Info ( "Skip dumping attachment data" )
} else if err := storage . Attachments . IterateObjects ( func ( objPath string , object storage . Object ) error {
2020-09-29 12:05:13 +03:00
info , err := object . Stat ( )
if err != nil {
return err
}
return w . Write ( archiver . File {
FileInfo : archiver . FileInfo {
FileInfo : info ,
CustomName : path . Join ( "data" , "attachments" , objPath ) ,
} ,
ReadCloser : object ,
} )
} ) ; err != nil {
fatal ( "Failed to dump attachments: %v" , err )
}
2020-05-01 04:30:31 +03:00
// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not.
if ctx . IsSet ( "skip-log" ) && ctx . Bool ( "skip-log" ) {
log . Info ( "Skip dumping log files" )
2020-11-28 05:42:08 +03:00
} else {
isExist , err := util . IsExist ( setting . LogRootPath )
if err != nil {
log . Error ( "Unable to check if %s exists. Error: %v" , setting . LogRootPath , err )
}
if isExist {
2021-02-08 04:00:12 +03:00
if err := addRecursiveExclude ( w , "log" , setting . LogRootPath , [ ] string { absFileName } , verbose ) ; err != nil {
2020-11-28 05:42:08 +03:00
fatal ( "Failed to include log: %v" , err )
}
2020-01-17 05:56:51 +03:00
}
2015-11-28 14:11:38 +03:00
}
2017-02-26 11:01:49 +03:00
2020-06-05 23:47:39 +03:00
if fileName != "-" {
if err = w . Close ( ) ; err != nil {
2020-08-11 23:05:34 +03:00
_ = util . Remove ( fileName )
2020-06-05 23:47:39 +03:00
fatal ( "Failed to save %s: %v" , fileName , err )
}
2014-05-02 05:21:46 +04:00
2020-06-05 23:47:39 +03:00
if err := os . Chmod ( fileName , 0600 ) ; err != nil {
log . Info ( "Can't change file access permissions mask to 0600: %v" , err )
}
2016-08-17 21:38:42 +03:00
}
2020-06-05 23:47:39 +03:00
if fileName != "-" {
log . Info ( "Finish dumping in file %s" , fileName )
} else {
log . Info ( "Finish dumping to stdout" )
2016-12-01 02:56:15 +03:00
}
2016-05-12 21:32:28 +03:00
return nil
2014-05-02 05:21:46 +04:00
}
2017-01-12 07:47:20 +03:00
2020-06-05 23:47:39 +03:00
func contains ( slice [ ] string , s string ) bool {
for _ , v := range slice {
if v == s {
return true
}
}
return false
}
// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
func addRecursiveExclude ( w archiver . Writer , insidePath , absPath string , excludeAbsPath [ ] string , verbose bool ) error {
2017-01-12 07:47:20 +03:00
absPath , err := filepath . Abs ( absPath )
if err != nil {
return err
}
dir , err := os . Open ( absPath )
if err != nil {
return err
}
defer dir . Close ( )
files , err := dir . Readdir ( 0 )
if err != nil {
return err
}
for _ , file := range files {
currentAbsPath := path . Join ( absPath , file . Name ( ) )
2020-06-05 23:47:39 +03:00
currentInsidePath := path . Join ( insidePath , file . Name ( ) )
2017-01-12 07:47:20 +03:00
if file . IsDir ( ) {
2020-06-05 23:47:39 +03:00
if ! contains ( excludeAbsPath , currentAbsPath ) {
if err := addFile ( w , currentInsidePath , currentAbsPath , false ) ; err != nil {
return err
}
if err = addRecursiveExclude ( w , currentInsidePath , currentAbsPath , excludeAbsPath , verbose ) ; err != nil {
2017-01-12 07:47:20 +03:00
return err
}
}
} else {
2020-06-05 23:47:39 +03:00
if err = addFile ( w , currentInsidePath , currentAbsPath , verbose ) ; err != nil {
2017-01-12 07:47:20 +03:00
return err
}
}
}
return nil
}