New: Backup options and restoration

This commit is contained in:
Mark McDowall 2018-01-12 17:52:42 -08:00 committed by Taloth Saldono
parent fdbe45c0ab
commit 81d6c0d210
22 changed files with 427 additions and 93 deletions

View File

@ -1,4 +1,5 @@
using System.Linq; using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@ -46,6 +47,10 @@ namespace NzbDrone.Api.Config
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script);
SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder));
SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7);
SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90);
} }
private HostConfigResource GetHostConfig() private HostConfigResource GetHostConfig()

View File

@ -1,4 +1,4 @@
using Sonarr.Http.REST; using Sonarr.Http.REST;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Update; using NzbDrone.Core.Update;
@ -33,6 +33,9 @@ namespace NzbDrone.Api.Config
public string ProxyPassword { get; set; } public string ProxyPassword { get; set; }
public string ProxyBypassFilter { get; set; } public string ProxyBypassFilter { get; set; }
public bool ProxyBypassLocalAddresses { get; set; } public bool ProxyBypassLocalAddresses { get; set; }
public string BackupFolder { get; set; }
public int BackupInterval { get; set; }
public int BackupRetention { get; set; }
} }
public static class HostConfigResourceMapper public static class HostConfigResourceMapper
@ -66,7 +69,10 @@ namespace NzbDrone.Api.Config
ProxyUsername = configService.ProxyUsername, ProxyUsername = configService.ProxyUsername,
ProxyPassword = configService.ProxyPassword, ProxyPassword = configService.ProxyPassword,
ProxyBypassFilter = configService.ProxyBypassFilter, ProxyBypassFilter = configService.ProxyBypassFilter,
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
BackupFolder = configService.BackupFolder,
BackupInterval = configService.BackupInterval,
BackupRetention = configService.BackupRetention
}; };
} }
} }

View File

@ -1,10 +1,10 @@
using System.IO; using System;
using System.IO;
using ICSharpCode.SharpZipLib.Core; using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common namespace NzbDrone.Common
{ {
@ -27,11 +27,10 @@ namespace NzbDrone.Common
{ {
_logger.Debug("Extracting archive [{0}] to [{1}]", compressedFile, destination); _logger.Debug("Extracting archive [{0}] to [{1}]", compressedFile, destination);
if (OsInfo.IsWindows) if (compressedFile.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase))
{ {
ExtractZip(compressedFile, destination); ExtractZip(compressedFile, destination);
} }
else else
{ {
ExtractTgz(compressedFile, destination); ExtractTgz(compressedFile, destination);

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -474,5 +474,13 @@ namespace NzbDrone.Common.Disk
} }
} }
} }
public void SaveStream(Stream stream, string path)
{
using (var fileStream = OpenWriteStream(path))
{
stream.CopyTo(fileStream);
}
}
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Security.AccessControl; using System.Security.AccessControl;
@ -48,5 +48,6 @@ namespace NzbDrone.Common.Disk
List<DirectoryInfo> GetDirectoryInfos(string path); List<DirectoryInfo> GetDirectoryInfos(string path);
List<FileInfo> GetFileInfos(string path); List<FileInfo> GetFileInfos(string path);
void RemoveEmptySubfolders(string path); void RemoveEmptySubfolders(string path);
void SaveStream(Stream stream, string path);
} }
} }

View File

@ -12,10 +12,10 @@ namespace NzbDrone.Common.Extensions
{ {
private const string APP_CONFIG_FILE = "config.xml"; private const string APP_CONFIG_FILE = "config.xml";
private const string DB = "sonarr.db"; private const string DB = "sonarr.db";
private const string DB_RESTORE = "sonarr.restore";
private const string LOG_DB = "logs.db"; private const string LOG_DB = "logs.db";
private const string NLOG_CONFIG_FILE = "nlog.config"; private const string NLOG_CONFIG_FILE = "nlog.config";
private const string UPDATE_CLIENT_EXE = "Sonarr.Update.exe"; private const string UPDATE_CLIENT_EXE = "Sonarr.Update.exe";
private const string BACKUP_FOLDER = "Backups";
private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "sonarr_update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "sonarr_update" + Path.DirectorySeparatorChar;
private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "Sonarr" + Path.DirectorySeparatorChar; private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "Sonarr" + Path.DirectorySeparatorChar;
@ -274,16 +274,16 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE); return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE);
} }
public static string GetBackupFolder(this IAppFolderInfo appFolderInfo)
{
return Path.Combine(GetAppDataPath(appFolderInfo), BACKUP_FOLDER);
}
public static string GetDatabase(this IAppFolderInfo appFolderInfo) public static string GetDatabase(this IAppFolderInfo appFolderInfo)
{ {
return Path.Combine(GetAppDataPath(appFolderInfo), DB); return Path.Combine(GetAppDataPath(appFolderInfo), DB);
} }
public static string GetDatabaseRestore(this IAppFolderInfo appFolderInfo)
{
return Path.Combine(GetAppDataPath(appFolderInfo), DB_RESTORE);
}
public static string GetLogDatabase(this IAppFolderInfo appFolderInfo) public static string GetLogDatabase(this IAppFolderInfo appFolderInfo)
{ {
return Path.Combine(GetAppDataPath(appFolderInfo), LOG_DB); return Path.Combine(GetAppDataPath(appFolderInfo), LOG_DB);

View File

@ -1,17 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Marr.Data;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
@ -21,6 +21,9 @@ namespace NzbDrone.Core.Backup
{ {
void Backup(BackupType backupType); void Backup(BackupType backupType);
List<Backup> GetBackups(); List<Backup> GetBackups();
void Restore(string backupFileName);
string GetBackupFolder();
string GetBackupFolder(BackupType backupType);
} }
public class BackupService : IBackupService, IExecute<BackupCommand> public class BackupService : IBackupService, IExecute<BackupCommand>
@ -31,11 +34,12 @@ namespace NzbDrone.Core.Backup
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IArchiveService _archiveService; private readonly IArchiveService _archiveService;
private readonly IConfigService _configService;
private readonly Logger _logger; private readonly Logger _logger;
private string _backupTempFolder; private string _backupTempFolder;
private static readonly Regex BackupFileRegex = new Regex(@"nzbdrone_backup_[._0-9]+\.zip", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static readonly Regex BackupFileRegex = new Regex(@"(nzbdrone|sonarr)_backup_[._0-9]+\.zip", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public BackupService(IMainDatabase maindDb, public BackupService(IMainDatabase maindDb,
IMakeDatabaseBackup makeDatabaseBackup, IMakeDatabaseBackup makeDatabaseBackup,
@ -43,6 +47,7 @@ namespace NzbDrone.Core.Backup
IDiskProvider diskProvider, IDiskProvider diskProvider,
IAppFolderInfo appFolderInfo, IAppFolderInfo appFolderInfo,
IArchiveService archiveService, IArchiveService archiveService,
IConfigService configService,
Logger logger) Logger logger)
{ {
_maindDb = maindDb; _maindDb = maindDb;
@ -51,9 +56,10 @@ namespace NzbDrone.Core.Backup
_diskProvider = diskProvider; _diskProvider = diskProvider;
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
_archiveService = archiveService; _archiveService = archiveService;
_configService = configService;
_logger = logger; _logger = logger;
_backupTempFolder = Path.Combine(_appFolderInfo.TempFolder, "nzbdrone_backup"); _backupTempFolder = Path.Combine(_appFolderInfo.TempFolder, "sonarr_backup");
} }
public void Backup(BackupType backupType) public void Backup(BackupType backupType)
@ -63,7 +69,7 @@ namespace NzbDrone.Core.Backup
_diskProvider.EnsureFolder(_backupTempFolder); _diskProvider.EnsureFolder(_backupTempFolder);
_diskProvider.EnsureFolder(GetBackupFolder(backupType)); _diskProvider.EnsureFolder(GetBackupFolder(backupType));
var backupFilename = string.Format("nzbdrone_backup_{0:yyyy.MM.dd_HH.mm.ss}.zip", DateTime.Now); var backupFilename = string.Format("sonarr_backup_{0:yyyy.MM.dd_HH.mm.ss}.zip", DateTime.Now);
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename); var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
Cleanup(); Cleanup();
@ -75,9 +81,15 @@ namespace NzbDrone.Core.Backup
BackupConfigFile(); BackupConfigFile();
BackupDatabase(); BackupDatabase();
CreateVersionInfo();
_logger.ProgressDebug("Creating backup zip"); _logger.ProgressDebug("Creating backup zip");
// Delete journal file created during database backup
_diskProvider.DeleteFile(Path.Combine(_backupTempFolder, "sonarr.db-journal"));
_archiveService.CreateZip(backupPath, _diskProvider.GetFiles(_backupTempFolder, SearchOption.TopDirectoryOnly)); _archiveService.CreateZip(backupPath, _diskProvider.GetFiles(_backupTempFolder, SearchOption.TopDirectoryOnly));
_logger.ProgressDebug("Backup zip created"); _logger.ProgressDebug("Backup zip created");
} }
@ -103,6 +115,68 @@ namespace NzbDrone.Core.Backup
return backups; return backups;
} }
public void Restore(string backupFileName)
{
if (backupFileName.EndsWith(".zip"))
{
var restoredFile = false;
var temporaryPath = Path.Combine(_appFolderInfo.TempFolder, "sonarr_backup_restore");
_archiveService.Extract(backupFileName, temporaryPath);
foreach (var file in _diskProvider.GetFiles(temporaryPath, SearchOption.TopDirectoryOnly))
{
var fileName = Path.GetFileName(file);
if (fileName.Equals("Config.xml", StringComparison.InvariantCultureIgnoreCase))
{
_diskProvider.MoveFile(file, _appFolderInfo.GetConfigPath(), true);
restoredFile = true;
}
if (fileName.Equals("nzbdrone.db", StringComparison.InvariantCultureIgnoreCase))
{
_diskProvider.MoveFile(file, _appFolderInfo.GetDatabaseRestore(), true);
restoredFile = true;
}
if (fileName.Equals("sonarr.db", StringComparison.InvariantCultureIgnoreCase))
{
_diskProvider.MoveFile(file, _appFolderInfo.GetDatabaseRestore(), true);
restoredFile = true;
}
}
if (!restoredFile)
{
throw new RestoreBackupFailedException(HttpStatusCode.NotFound, "Unable to restore database file from backup");
}
_diskProvider.DeleteFolder(temporaryPath, true);
return;
}
_diskProvider.MoveFile(backupFileName, _appFolderInfo.GetDatabaseRestore(), true);
}
public string GetBackupFolder()
{
var backupFolder = _configService.BackupFolder;
if (Path.IsPathRooted(backupFolder))
{
return backupFolder;
}
return Path.Combine(_appFolderInfo.GetAppDataPath(), backupFolder);
}
public string GetBackupFolder(BackupType backupType)
{
return Path.Combine(GetBackupFolder(), backupType.ToString().ToLower());
}
private void Cleanup() private void Cleanup()
{ {
if (_diskProvider.FolderExists(_backupTempFolder)) if (_diskProvider.FolderExists(_backupTempFolder))
@ -128,16 +202,25 @@ namespace NzbDrone.Core.Backup
_diskTransferService.TransferFile(configFile, tempConfigFile, TransferMode.Copy); _diskTransferService.TransferFile(configFile, tempConfigFile, TransferMode.Copy);
} }
private void CreateVersionInfo()
{
var builder = new StringBuilder();
builder.AppendLine(BuildInfo.Version.ToString());
}
private void CleanupOldBackups(BackupType backupType) private void CleanupOldBackups(BackupType backupType)
{ {
_logger.Debug("Cleaning up old backup files"); var retention = _configService.BackupRetention;
_logger.Debug("Cleaning up backup files older than {0} days", retention);
var files = GetBackupFiles(GetBackupFolder(backupType)); var files = GetBackupFiles(GetBackupFolder(backupType));
foreach (var file in files) foreach (var file in files)
{ {
var lastWriteTime = _diskProvider.FileGetLastWrite(file); var lastWriteTime = _diskProvider.FileGetLastWrite(file);
if (lastWriteTime.AddDays(28) < DateTime.UtcNow) if (lastWriteTime.AddDays(retention) < DateTime.UtcNow)
{ {
_logger.Debug("Deleting old backup file: {0}", file); _logger.Debug("Deleting old backup file: {0}", file);
_diskProvider.DeleteFile(file); _diskProvider.DeleteFile(file);
@ -147,11 +230,6 @@ namespace NzbDrone.Core.Backup
_logger.Debug("Finished cleaning up old backup files"); _logger.Debug("Finished cleaning up old backup files");
} }
private string GetBackupFolder(BackupType backupType)
{
return Path.Combine(_appFolderInfo.GetBackupFolder(), backupType.ToString().ToLower());
}
private IEnumerable<string> GetBackupFiles(string path) private IEnumerable<string> GetBackupFiles(string path)
{ {
var files = _diskProvider.GetFiles(path, SearchOption.TopDirectoryOnly); var files = _diskProvider.GetFiles(path, SearchOption.TopDirectoryOnly);

View File

@ -0,0 +1,16 @@
using System.Net;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.Backup
{
public class RestoreBackupFailedException : NzbDroneClientException
{
public RestoreBackupFailedException(HttpStatusCode statusCode, string message, params object[] args) : base(statusCode, message, args)
{
}
public RestoreBackupFailedException(HttpStatusCode statusCode, string message) : base(statusCode, message)
{
}
}
}

View File

@ -4,7 +4,6 @@ using System.Globalization;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -331,6 +330,12 @@ namespace NzbDrone.Core.Configuration
public bool ProxyBypassLocalAddresses => GetValueBoolean("ProxyBypassLocalAddresses", true); public bool ProxyBypassLocalAddresses => GetValueBoolean("ProxyBypassLocalAddresses", true);
public string BackupFolder => GetValue("BackupFolder", "Backups");
public int BackupInterval => GetValueInt("BackupInterval", 7);
public int BackupRetention => GetValueInt("BackupRetention", 28);
private string GetValue(string key) private string GetValue(string key)
{ {
return GetValue(key, string.Empty); return GetValue(key, string.Empty);

View File

@ -76,5 +76,10 @@ namespace NzbDrone.Core.Configuration
string ProxyPassword { get; } string ProxyPassword { get; }
string ProxyBypassFilter { get; } string ProxyBypassFilter { get; }
bool ProxyBypassLocalAddresses { get; } bool ProxyBypassLocalAddresses { get; }
// Backups
string BackupFolder { get; }
int BackupInterval { get; }
int BackupRetention { get; }
} }
} }

View File

@ -0,0 +1,56 @@
using System;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Core.Datastore
{
public interface IRestoreDatabase
{
void Restore();
}
public class DatabaseRestorationService : IRestoreDatabase
{
private readonly IDiskProvider _diskProvider;
private readonly IAppFolderInfo _appFolderInfo;
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DatabaseRestorationService));
public DatabaseRestorationService(IDiskProvider diskProvider, IAppFolderInfo appFolderInfo)
{
_diskProvider = diskProvider;
_appFolderInfo = appFolderInfo;
}
public void Restore()
{
var dbRestorePath = _appFolderInfo.GetDatabaseRestore();
if (!_diskProvider.FileExists(dbRestorePath))
{
return;
}
try
{
Logger.Info("Restoring Database");
var dbPath = _appFolderInfo.GetDatabase();
_diskProvider.DeleteFile(dbPath + "-shm");
_diskProvider.DeleteFile(dbPath + "-wal");
_diskProvider.DeleteFile(dbPath + "-journal");
_diskProvider.DeleteFile(dbPath);
_diskProvider.MoveFile(dbRestorePath, dbPath);
}
catch (Exception e)
{
Logger.Error(e, "Failed to restore database");
throw;
}
}
}
}

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Data.SQLite; using System.Data.SQLite;
using Marr.Data; using Marr.Data;
using Marr.Data.Reflection; using Marr.Data.Reflection;
@ -24,6 +24,7 @@ namespace NzbDrone.Core.Datastore
private readonly IMigrationController _migrationController; private readonly IMigrationController _migrationController;
private readonly IConnectionStringFactory _connectionStringFactory; private readonly IConnectionStringFactory _connectionStringFactory;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IRestoreDatabase _restoreDatabaseService;
static DbFactory() static DbFactory()
{ {
@ -54,11 +55,13 @@ namespace NzbDrone.Core.Datastore
public DbFactory(IMigrationController migrationController, public DbFactory(IMigrationController migrationController,
IConnectionStringFactory connectionStringFactory, IConnectionStringFactory connectionStringFactory,
IDiskProvider diskProvider) IDiskProvider diskProvider,
IRestoreDatabase restoreDatabaseService)
{ {
_migrationController = migrationController; _migrationController = migrationController;
_connectionStringFactory = connectionStringFactory; _connectionStringFactory = connectionStringFactory;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_restoreDatabaseService = restoreDatabaseService;
} }
public IDatabase Create(MigrationType migrationType = MigrationType.Main) public IDatabase Create(MigrationType migrationType = MigrationType.Main)
@ -70,17 +73,20 @@ namespace NzbDrone.Core.Datastore
{ {
string connectionString; string connectionString;
switch (migrationContext.MigrationType) switch (migrationContext.MigrationType)
{ {
case MigrationType.Main: case MigrationType.Main:
{ {
connectionString = _connectionStringFactory.MainDbConnectionString; connectionString = _connectionStringFactory.MainDbConnectionString;
CreateMain(connectionString, migrationContext);
break; break;
} }
case MigrationType.Log: case MigrationType.Log:
{ {
connectionString = _connectionStringFactory.LogDbConnectionString; connectionString = _connectionStringFactory.LogDbConnectionString;
CreateLog(connectionString, migrationContext);
break; break;
} }
default: default:
@ -89,44 +95,6 @@ namespace NzbDrone.Core.Datastore
} }
} }
try
{
_migrationController.Migrate(connectionString, migrationContext);
}
catch (SQLiteException ex)
{
var fileName = _connectionStringFactory.GetDatabasePath(connectionString);
if (migrationContext.MigrationType == MigrationType.Log)
{
Logger.Error(ex, "Logging database is corrupt, attempting to recreate it automatically");
try
{
_diskProvider.DeleteFile(fileName + "-shm");
_diskProvider.DeleteFile(fileName + "-wal");
_diskProvider.DeleteFile(fileName + "-journal");
_diskProvider.DeleteFile(fileName);
}
catch (Exception)
{
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
}
_migrationController.Migrate(connectionString, migrationContext);
}
else
{
if (OsInfo.IsOsx)
{
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-use-sonarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", ex, fileName);
}
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-am-getting-an-error-database-disk-image-is-malformed", ex, fileName);
}
}
var db = new Database(migrationContext.MigrationType.ToString(), () => var db = new Database(migrationContext.MigrationType.ToString(), () =>
{ {
var dataMapper = new DataMapper(SQLiteFactory.Instance, connectionString) var dataMapper = new DataMapper(SQLiteFactory.Instance, connectionString)
@ -139,5 +107,53 @@ namespace NzbDrone.Core.Datastore
return db; return db;
} }
private void CreateMain(string connectionString, MigrationContext migrationContext)
{
try
{
_restoreDatabaseService.Restore();
_migrationController.Migrate(connectionString, migrationContext);
}
catch (SQLiteException e)
{
var fileName = _connectionStringFactory.GetDatabasePath(connectionString);
if (OsInfo.IsOsx)
{
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-use-sonarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName);
}
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
}
}
private void CreateLog(string connectionString, MigrationContext migrationContext)
{
try
{
_migrationController.Migrate(connectionString, migrationContext);
}
catch (SQLiteException e)
{
var fileName = _connectionStringFactory.GetDatabasePath(connectionString);
Logger.Error(e, "Logging database is corrupt, attempting to recreate it automatically");
try
{
_diskProvider.DeleteFile(fileName + "-shm");
_diskProvider.DeleteFile(fileName + "-wal");
_diskProvider.DeleteFile(fileName + "-journal");
_diskProvider.DeleteFile(fileName);
}
catch (Exception)
{
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
}
_migrationController.Migrate(connectionString, migrationContext);
}
}
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -67,7 +67,12 @@ namespace NzbDrone.Core.Jobs
new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName},
new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName},
new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName},
new ScheduledTask{ Interval = 7*24*60, TypeName = typeof(BackupCommand).FullName},
new ScheduledTask
{
Interval = GetBackupInterval(),
TypeName = typeof(BackupCommand).FullName
},
new ScheduledTask new ScheduledTask
{ {
@ -104,6 +109,13 @@ namespace NzbDrone.Core.Jobs
} }
} }
private int GetBackupInterval()
{
var interval = _configService.BackupInterval;
return interval * 60 * 24;
}
private int GetRssSyncInterval() private int GetRssSyncInterval()
{ {
var interval = _configService.RssSyncInterval; var interval = _configService.RssSyncInterval;

View File

@ -125,6 +125,7 @@
<Compile Include="Backup\BackupCommand.cs" /> <Compile Include="Backup\BackupCommand.cs" />
<Compile Include="Backup\BackupService.cs" /> <Compile Include="Backup\BackupService.cs" />
<Compile Include="Backup\MakeDatabaseBackup.cs" /> <Compile Include="Backup\MakeDatabaseBackup.cs" />
<Compile Include="Backup\RestoreBackupFailedException.cs" />
<Compile Include="Blacklisting\Blacklist.cs" /> <Compile Include="Blacklisting\Blacklist.cs" />
<Compile Include="Blacklisting\BlacklistRepository.cs" /> <Compile Include="Blacklisting\BlacklistRepository.cs" />
<Compile Include="Blacklisting\BlacklistService.cs" /> <Compile Include="Blacklisting\BlacklistService.cs" />
@ -185,6 +186,7 @@
<Compile Include="Datastore\Converters\UtcConverter.cs" /> <Compile Include="Datastore\Converters\UtcConverter.cs" />
<Compile Include="Datastore\CorruptDatabaseException.cs" /> <Compile Include="Datastore\CorruptDatabaseException.cs" />
<Compile Include="Datastore\Database.cs" /> <Compile Include="Datastore\Database.cs" />
<Compile Include="Datastore\DatabaseRestorationService.cs" />
<Compile Include="Datastore\DbFactory.cs" /> <Compile Include="Datastore\DbFactory.cs" />
<Compile Include="Datastore\Events\ModelEvent.cs" /> <Compile Include="Datastore\Events\ModelEvent.cs" />
<Compile Include="Datastore\Extensions\MappingExtensions.cs" /> <Compile Include="Datastore\Extensions\MappingExtensions.cs" />

View File

@ -104,7 +104,5 @@ namespace NzbDrone.Host
} }
} }
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System.Linq; using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@ -46,6 +47,11 @@ namespace Sonarr.Api.V3.Config
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script);
SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder));
SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7);
SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90);
} }
private HostConfigResource GetHostConfig() private HostConfigResource GetHostConfig()
@ -75,6 +81,7 @@ namespace Sonarr.Api.V3.Config
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configFileProvider.SaveConfigDictionary(dictionary); _configFileProvider.SaveConfigDictionary(dictionary);
_configService.SaveConfigDictionary(dictionary);
if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace()) if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace())
{ {

View File

@ -1,4 +1,4 @@
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Update; using NzbDrone.Core.Update;
@ -33,6 +33,9 @@ namespace Sonarr.Api.V3.Config
public string ProxyPassword { get; set; } public string ProxyPassword { get; set; }
public string ProxyBypassFilter { get; set; } public string ProxyBypassFilter { get; set; }
public bool ProxyBypassLocalAddresses { get; set; } public bool ProxyBypassLocalAddresses { get; set; }
public string BackupFolder { get; set; }
public int BackupInterval { get; set; }
public int BackupRetention { get; set; }
} }
public static class HostConfigResourceMapper public static class HostConfigResourceMapper
@ -66,7 +69,10 @@ namespace Sonarr.Api.V3.Config
ProxyUsername = configService.ProxyUsername, ProxyUsername = configService.ProxyUsername,
ProxyPassword = configService.ProxyPassword, ProxyPassword = configService.ProxyPassword,
ProxyBypassFilter = configService.ProxyBypassFilter, ProxyBypassFilter = configService.ProxyBypassFilter,
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
BackupFolder = configService.BackupFolder,
BackupInterval = configService.BackupInterval,
BackupRetention = configService.BackupRetention
}; };
} }
} }

View File

@ -1,19 +1,39 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Nancy;
using NzbDrone.Common.Crypto;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Backup; using NzbDrone.Core.Backup;
using Sonarr.Http; using Sonarr.Http;
using Sonarr.Http.Extensions;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.System.Backup namespace Sonarr.Api.V3.System.Backup
{ {
public class BackupModule : SonarrRestModule<BackupResource> public class BackupModule : SonarrRestModule<BackupResource>
{ {
private readonly IBackupService _backupService; private readonly IBackupService _backupService;
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
public BackupModule(IBackupService backupService) : base("system/backup") private static readonly List<string> ValidExtensions = new List<string> { ".zip", ".db", ".xml" };
public BackupModule(IBackupService backupService,
IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider)
: base("system/backup")
{ {
_backupService = backupService; _backupService = backupService;
_appFolderInfo = appFolderInfo;
_diskProvider = diskProvider;
GetResourceAll = GetBackupFiles; GetResourceAll = GetBackupFiles;
DeleteResource = DeleteBackup;
Post[@"/restore/(?<id>[\d]{1,10})"] = x => Restore((int)x.Id);
Post["/restore/upload"] = x => UploadAndRestore();
} }
public List<BackupResource> GetBackupFiles() public List<BackupResource> GetBackupFiles()
@ -22,7 +42,7 @@ namespace Sonarr.Api.V3.System.Backup
return backups.Select(b => new BackupResource return backups.Select(b => new BackupResource
{ {
Id = b.Name.GetHashCode(), Id = GetBackupId(b),
Name = b.Name, Name = b.Name,
Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}", Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}",
Type = b.Type, Type = b.Type,
@ -31,5 +51,83 @@ namespace Sonarr.Api.V3.System.Backup
.OrderByDescending(b => b.Time) .OrderByDescending(b => b.Time)
.ToList(); .ToList();
} }
private void DeleteBackup(int id)
{
var backup = GetBackup(id);
var path = GetBackupPath(backup);
if (!_diskProvider.FileExists(path))
{
throw new NotFoundException();
}
_diskProvider.DeleteFile(path);
}
public Response Restore(int id)
{
var backup = GetBackup(id);
if (backup == null)
{
throw new NotFoundException();
}
var path = GetBackupPath(backup);
_backupService.Restore(path);
return new
{
RestartRequired = true
}.AsResponse();
}
public Response UploadAndRestore()
{
var files = Context.Request.Files.ToList();
if (files.Empty())
{
throw new BadRequestException("file must be provided");
}
var file = files.First();
var extension = Path.GetExtension(file.Name);
if (!ValidExtensions.Contains(extension))
{
throw new UnsupportedMediaTypeException($"Invalid extension, must be one of: {ValidExtensions.Join(", ")}");
}
var path = Path.Combine(_appFolderInfo.TempFolder, $"sonarr_backup_restore{extension}");
_diskProvider.SaveStream(file.Value, path);
_backupService.Restore(path);
// Cleanup restored file
_diskProvider.DeleteFile(path);
return new
{
RestartRequired = true
}.AsResponse();
}
private string GetBackupPath(NzbDrone.Core.Backup.Backup backup)
{
return Path.Combine(_backupService.GetBackupFolder(backup.Type), backup.Name);
}
private int GetBackupId(NzbDrone.Core.Backup.Backup backup)
{
return HashConverter.GetHashInt31($"backup-{backup.Type}-{backup.Name}");
}
private NzbDrone.Core.Backup.Backup GetBackup(int id)
{
return _backupService.GetBackups().SingleOrDefault(b => GetBackupId(b) == id);
}
} }
} }

View File

@ -1,4 +1,5 @@
using Nancy; using System.Threading.Tasks;
using Nancy;
using Nancy.Routing; using Nancy.Routing;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -81,14 +82,14 @@ namespace Sonarr.Api.V3.System
private Response Shutdown() private Response Shutdown()
{ {
_lifecycleService.Shutdown(); Task.Factory.StartNew(() => _lifecycleService.Shutdown());
return "".AsResponse(); return new { ShuttingDown = true }.AsResponse();
} }
private Response Restart() private Response Restart()
{ {
_lifecycleService.Restart(); Task.Factory.StartNew(() => _lifecycleService.Restart());
return "".AsResponse(); return new { Restarting = true }.AsResponse();
} }
} }
} }

View File

@ -2,30 +2,31 @@ using System.IO;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Core.Backup;
namespace Sonarr.Http.Frontend.Mappers namespace Sonarr.Http.Frontend.Mappers
{ {
public class BackupFileMapper : StaticResourceMapperBase public class BackupFileMapper : StaticResourceMapperBase
{ {
private readonly IBackupService _backupService;
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
public BackupFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) public BackupFileMapper(IBackupService backupService, IDiskProvider diskProvider, Logger logger)
: base(diskProvider, logger) : base(diskProvider, logger)
{ {
_appFolderInfo = appFolderInfo; _backupService = backupService;
} }
public override string Map(string resourceUrl) public override string Map(string resourceUrl)
{ {
var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar); var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(_appFolderInfo.GetBackupFolder(), path); return Path.Combine(_backupService.GetBackupFolder(), path);
} }
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
{ {
return resourceUrl.StartsWith("/backup/") && resourceUrl.ContainsIgnoreCase("nzbdrone_backup_") && resourceUrl.EndsWith(".zip"); return resourceUrl.StartsWith("/backup/") && BackupService.BackupFileRegex.IsMatch(resourceUrl);
} }
} }
} }

View File

@ -0,0 +1,13 @@
using Nancy;
using Sonarr.Http.Exceptions;
namespace Sonarr.Http.REST
{
public class UnsupportedMediaTypeException : ApiException
{
public UnsupportedMediaTypeException(object content = null)
: base(HttpStatusCode.UnsupportedMediaType, content)
{
}
}
}

View File

@ -121,6 +121,7 @@
<Compile Include="PagingResourceFilter.cs" /> <Compile Include="PagingResourceFilter.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ResourceChangeMessage.cs" /> <Compile Include="ResourceChangeMessage.cs" />
<Compile Include="REST\UnsupportedMediaTypeException.cs" />
<Compile Include="REST\BadRequestException.cs" /> <Compile Include="REST\BadRequestException.cs" />
<Compile Include="REST\MethodNotAllowedException.cs" /> <Compile Include="REST\MethodNotAllowedException.cs" />
<Compile Include="REST\NotFoundException.cs" /> <Compile Include="REST\NotFoundException.cs" />