2024-10-01 15:58:54 -08:00
package cmd
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/spf13/cobra"
)
var (
backupCount int
backupDir string
force bool
restorePath string
)
func init ( ) {
rootCmd . AddCommand ( backupRoot )
backupCmd . Flags ( ) . StringVarP ( & backupDir , "backup-dir" , "d" , "" , "directory to manually make backup" )
backupRoot . AddCommand ( backupCmd )
pruneCmd . Flags ( ) . StringVarP ( & backupDir , "backup-dir" , "d" , "" , "directory holding Navidrome backups" )
pruneCmd . Flags ( ) . IntVarP ( & backupCount , "keep-count" , "k" , - 1 , "specify the number of backups to keep. 0 remove ALL backups, and negative values mean to use the default from configuration" )
pruneCmd . Flags ( ) . BoolVarP ( & force , "force" , "f" , false , "bypass warning when backup count is zero" )
backupRoot . AddCommand ( pruneCmd )
restoreCommand . Flags ( ) . StringVarP ( & restorePath , "backup-file" , "b" , "" , "path of backup database to restore" )
restoreCommand . Flags ( ) . BoolVarP ( & force , "force" , "f" , false , "bypass restore warning" )
2024-12-22 12:41:40 -09:00
_ = restoreCommand . MarkFlagRequired ( "backup-file" )
2024-10-01 15:58:54 -08:00
backupRoot . AddCommand ( restoreCommand )
}
var (
backupRoot = & cobra . Command {
Use : "backup" ,
Aliases : [ ] string { "bkp" } ,
Short : "Create, restore and prune database backups" ,
Long : "Create, restore and prune database backups" ,
}
backupCmd = & cobra . Command {
Use : "create" ,
Short : "Create a backup database" ,
Long : "Manually backup Navidrome database. This will ignore BackupCount" ,
Run : func ( cmd * cobra . Command , _ [ ] string ) {
runBackup ( cmd . Context ( ) )
} ,
}
pruneCmd = & cobra . Command {
Use : "prune" ,
Short : "Prune database backups" ,
Long : "Manually prune database backups according to backup rules" ,
Run : func ( cmd * cobra . Command , _ [ ] string ) {
runPrune ( cmd . Context ( ) )
} ,
}
restoreCommand = & cobra . Command {
Use : "restore" ,
Short : "Restore Navidrome database" ,
Long : "Restore Navidrome database from a backup. This must be done offline" ,
Run : func ( cmd * cobra . Command , _ [ ] string ) {
runRestore ( cmd . Context ( ) )
} ,
}
)
func runBackup ( ctx context . Context ) {
if backupDir != "" {
conf . Server . Backup . Path = backupDir
}
idx := strings . LastIndex ( conf . Server . DbPath , "?" )
var path string
if idx == - 1 {
path = conf . Server . DbPath
} else {
path = conf . Server . DbPath [ : idx ]
}
if _ , err := os . Stat ( path ) ; os . IsNotExist ( err ) {
log . Fatal ( "No existing database" , "path" , path )
return
}
start := time . Now ( )
2024-11-18 04:52:13 -09:00
path , err := db . Backup ( ctx )
2024-10-01 15:58:54 -08:00
if err != nil {
log . Fatal ( "Error backing up database" , "backup path" , conf . Server . BasePath , err )
}
elapsed := time . Since ( start )
log . Info ( "Backup complete" , "elapsed" , elapsed , "path" , path )
}
func runPrune ( ctx context . Context ) {
if backupDir != "" {
conf . Server . Backup . Path = backupDir
}
if backupCount != - 1 {
conf . Server . Backup . Count = backupCount
}
if conf . Server . Backup . Count == 0 && ! force {
fmt . Println ( "Warning: pruning ALL backups" )
fmt . Printf ( "Please enter YES (all caps) to continue: " )
var input string
_ , err := fmt . Scanln ( & input )
if input != "YES" || err != nil {
2024-12-22 12:38:59 -09:00
log . Warn ( "Prune cancelled" )
2024-10-01 15:58:54 -08:00
return
}
}
idx := strings . LastIndex ( conf . Server . DbPath , "?" )
var path string
if idx == - 1 {
path = conf . Server . DbPath
} else {
path = conf . Server . DbPath [ : idx ]
}
if _ , err := os . Stat ( path ) ; os . IsNotExist ( err ) {
log . Fatal ( "No existing database" , "path" , path )
return
}
start := time . Now ( )
2024-11-18 04:52:13 -09:00
count , err := db . Prune ( ctx )
2024-10-01 15:58:54 -08:00
if err != nil {
log . Fatal ( "Error pruning up database" , "backup path" , conf . Server . BasePath , err )
}
elapsed := time . Since ( start )
log . Info ( "Prune complete" , "elapsed" , elapsed , "successfully pruned" , count )
}
func runRestore ( ctx context . Context ) {
idx := strings . LastIndex ( conf . Server . DbPath , "?" )
var path string
if idx == - 1 {
path = conf . Server . DbPath
} else {
path = conf . Server . DbPath [ : idx ]
}
if _ , err := os . Stat ( path ) ; os . IsNotExist ( err ) {
log . Fatal ( "No existing database" , "path" , path )
return
}
if ! force {
fmt . Println ( "Warning: restoring the Navidrome database should only be done offline, especially if your backup is very old." )
fmt . Printf ( "Please enter YES (all caps) to continue: " )
var input string
_ , err := fmt . Scanln ( & input )
if input != "YES" || err != nil {
log . Warn ( "Restore cancelled" )
return
}
}
start := time . Now ( )
2024-11-18 04:52:13 -09:00
err := db . Restore ( ctx , restorePath )
2024-10-01 15:58:54 -08:00
if err != nil {
2024-12-22 12:41:40 -09:00
log . Fatal ( "Error restoring database" , "backup path" , conf . Server . BasePath , err )
2024-10-01 15:58:54 -08:00
}
elapsed := time . Since ( start )
log . Info ( "Restore complete" , "elapsed" , elapsed )
}