mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-12-25 02:30:20 +02:00
Backups
New: Backup drone's database and configuration from the UI New: Download Backup files from the UI Fixed: Run a database backup before upgrade
This commit is contained in:
parent
d74e461aea
commit
c5bd8b27fb
1
.gitignore
vendored
1
.gitignore
vendored
@ -84,7 +84,6 @@ Generated_Code #added for RIA/Silverlight projects
|
|||||||
# Backup & report files from converting an old project file to a newer
|
# Backup & report files from converting an old project file to a newer
|
||||||
# Visual Studio version. Backup files are not needed, because we have git ;-)
|
# Visual Studio version. Backup files are not needed, because we have git ;-)
|
||||||
_UpgradeReport_Files/
|
_UpgradeReport_Files/
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
UpgradeLog*.XML
|
||||||
|
|
||||||
# SQL Server files
|
# SQL Server files
|
||||||
|
31
src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs
Normal file
31
src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.IO;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.Frontend.Mappers
|
||||||
|
{
|
||||||
|
public class BackupFileMapper : StaticResourceMapperBase
|
||||||
|
{
|
||||||
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
|
|
||||||
|
public BackupFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger)
|
||||||
|
: base(diskProvider, logger)
|
||||||
|
{
|
||||||
|
_appFolderInfo = appFolderInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string Map(string resourceUrl)
|
||||||
|
{
|
||||||
|
var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
return Path.Combine(_appFolderInfo.GetBackupFolder(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanHandle(string resourceUrl)
|
||||||
|
{
|
||||||
|
return resourceUrl.StartsWith("/backup/") && resourceUrl.ContainsIgnoreCase("nzbdrone_backup_") && resourceUrl.EndsWith(".zip");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -131,6 +131,7 @@
|
|||||||
<Compile Include="Extensions\RequestExtensions.cs" />
|
<Compile Include="Extensions\RequestExtensions.cs" />
|
||||||
<Compile Include="Frontend\IsCacheableSpecification.cs" />
|
<Compile Include="Frontend\IsCacheableSpecification.cs" />
|
||||||
<Compile Include="Frontend\Mappers\UpdateLogFileMapper.cs" />
|
<Compile Include="Frontend\Mappers\UpdateLogFileMapper.cs" />
|
||||||
|
<Compile Include="Frontend\Mappers\BackupFileMapper.cs" />
|
||||||
<Compile Include="Frontend\Mappers\FaviconMapper.cs" />
|
<Compile Include="Frontend\Mappers\FaviconMapper.cs" />
|
||||||
<Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" />
|
<Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" />
|
||||||
<Compile Include="Frontend\Mappers\LogFileMapper.cs" />
|
<Compile Include="Frontend\Mappers\LogFileMapper.cs" />
|
||||||
@ -165,6 +166,8 @@
|
|||||||
<Compile Include="Mapping\ResourceMappingException.cs" />
|
<Compile Include="Mapping\ResourceMappingException.cs" />
|
||||||
<Compile Include="Mapping\ValueInjectorExtensions.cs" />
|
<Compile Include="Mapping\ValueInjectorExtensions.cs" />
|
||||||
<Compile Include="Series\AlternateTitleResource.cs" />
|
<Compile Include="Series\AlternateTitleResource.cs" />
|
||||||
|
<Compile Include="System\Backup\BackupModule.cs" />
|
||||||
|
<Compile Include="System\Backup\BackupResource.cs" />
|
||||||
<Compile Include="Update\UpdateResource.cs" />
|
<Compile Include="Update\UpdateResource.cs" />
|
||||||
<Compile Include="Wanted\CutoffModule.cs" />
|
<Compile Include="Wanted\CutoffModule.cs" />
|
||||||
<Compile Include="Wanted\LegacyMissingModule.cs" />
|
<Compile Include="Wanted\LegacyMissingModule.cs" />
|
||||||
|
32
src/NzbDrone.Api/System/Backup/BackupModule.cs
Normal file
32
src/NzbDrone.Api/System/Backup/BackupModule.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Backup;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.System.Backup
|
||||||
|
{
|
||||||
|
public class BackupModule : NzbDroneRestModule<BackupResource>
|
||||||
|
{
|
||||||
|
private readonly IBackupService _backupService;
|
||||||
|
|
||||||
|
public BackupModule(IBackupService backupService) : base("system/backup")
|
||||||
|
{
|
||||||
|
_backupService = backupService;
|
||||||
|
GetResourceAll = GetBackupFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BackupResource> GetBackupFiles()
|
||||||
|
{
|
||||||
|
var backups = _backupService.GetBackups();
|
||||||
|
|
||||||
|
return backups.Select(b => new BackupResource
|
||||||
|
{
|
||||||
|
Id = b.Path.GetHashCode(),
|
||||||
|
Name = Path.GetFileName(b.Path),
|
||||||
|
Path = b.Path,
|
||||||
|
Type = b.Type,
|
||||||
|
Time = b.Time
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/NzbDrone.Api/System/Backup/BackupResource.cs
Normal file
14
src/NzbDrone.Api/System/Backup/BackupResource.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using NzbDrone.Api.REST;
|
||||||
|
using NzbDrone.Core.Backup;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.System.Backup
|
||||||
|
{
|
||||||
|
public class BackupResource : RestResource
|
||||||
|
{
|
||||||
|
public String Name { get; set; }
|
||||||
|
public String Path { get; set; }
|
||||||
|
public BackupType Type { get; set; }
|
||||||
|
public DateTime Time { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -172,7 +172,7 @@ public void should_be_able_to_delete_directory_with_read_only_file()
|
|||||||
public void empty_folder_should_return_folder_modified_date()
|
public void empty_folder_should_return_folder_modified_date()
|
||||||
{
|
{
|
||||||
var tempfolder = new DirectoryInfo(TempFolder);
|
var tempfolder = new DirectoryInfo(TempFolder);
|
||||||
Subject.FolderGetLastWrite(TempFolder).Should().Be(tempfolder.LastWriteTimeUtc);
|
Subject.FolderGetLastWriteUtc(TempFolder).Should().Be(tempfolder.LastWriteTimeUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -189,8 +189,8 @@ public void folder_should_return_correct_value_for_last_write()
|
|||||||
|
|
||||||
Subject.WriteAllText(testFile, "Test");
|
Subject.WriteAllText(testFile, "Test");
|
||||||
|
|
||||||
Subject.FolderGetLastWrite(TempFolder).Should().BeOnOrAfter(DateTime.UtcNow.AddMinutes(-1));
|
Subject.FolderGetLastWriteUtc(TempFolder).Should().BeOnOrAfter(DateTime.UtcNow.AddMinutes(-1));
|
||||||
Subject.FolderGetLastWrite(TempFolder).Should().BeBefore(DateTime.UtcNow.AddMinutes(1));
|
Subject.FolderGetLastWriteUtc(TempFolder).Should().BeBefore(DateTime.UtcNow.AddMinutes(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -238,7 +238,7 @@ public void should_be_able_to_set_permission_from_parrent()
|
|||||||
[Explicit]
|
[Explicit]
|
||||||
public void check_last_write()
|
public void check_last_write()
|
||||||
{
|
{
|
||||||
Console.WriteLine(Subject.FolderGetLastWrite(GetFilledTempFolder().FullName));
|
Console.WriteLine(Subject.FolderGetLastWriteUtc(GetFilledTempFolder().FullName));
|
||||||
Console.WriteLine(GetFilledTempFolder().LastWriteTimeUtc);
|
Console.WriteLine(GetFilledTempFolder().LastWriteTimeUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ namespace NzbDrone.Common
|
|||||||
public interface IArchiveService
|
public interface IArchiveService
|
||||||
{
|
{
|
||||||
void Extract(string compressedFile, string destination);
|
void Extract(string compressedFile, string destination);
|
||||||
|
void ExtractZip(string compressedFile, string destination);
|
||||||
|
void CreateZip(string path, params string[] files);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ArchiveService : IArchiveService
|
public class ArchiveService : IArchiveService
|
||||||
@ -40,7 +42,22 @@ public void Extract(string compressedFile, string destination)
|
|||||||
_logger.Debug("Extraction complete.");
|
_logger.Debug("Extraction complete.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExtractZip(string compressedFile, string destination)
|
public void CreateZip(string path, params string[] files)
|
||||||
|
{
|
||||||
|
using (var zipFile = ZipFile.Create(path))
|
||||||
|
{
|
||||||
|
zipFile.BeginUpdate();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
zipFile.Add(file, Path.GetFileName(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile.CommitUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExtractZip(string compressedFile, string destination)
|
||||||
{
|
{
|
||||||
using (var fileStream = File.OpenRead(compressedFile))
|
using (var fileStream = File.OpenRead(compressedFile))
|
||||||
{
|
{
|
@ -33,7 +33,7 @@ public DateTime FolderGetCreationTimeUtc(string path)
|
|||||||
return new DirectoryInfo(path).CreationTimeUtc;
|
return new DirectoryInfo(path).CreationTimeUtc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime FolderGetLastWrite(string path)
|
public DateTime FolderGetLastWriteUtc(string path)
|
||||||
{
|
{
|
||||||
CheckFolderExists(path);
|
CheckFolderExists(path);
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ public interface IDiskProvider
|
|||||||
void SetPermissions(string path, string mask, string user, string group);
|
void SetPermissions(string path, string mask, string user, string group);
|
||||||
long? GetTotalSize(string path);
|
long? GetTotalSize(string path);
|
||||||
DateTime FolderGetCreationTimeUtc(string path);
|
DateTime FolderGetCreationTimeUtc(string path);
|
||||||
DateTime FolderGetLastWrite(string path);
|
DateTime FolderGetLastWriteUtc(string path);
|
||||||
DateTime FileGetCreationTimeUtc(string path);
|
DateTime FileGetCreationTimeUtc(string path);
|
||||||
DateTime FileGetLastWrite(string path);
|
DateTime FileGetLastWrite(string path);
|
||||||
DateTime FileGetLastWriteUtc(string path);
|
DateTime FileGetLastWriteUtc(string path);
|
||||||
|
@ -59,8 +59,8 @@
|
|||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="ArchiveProvider.cs" />
|
|
||||||
<Compile Include="ConvertBase32.cs" />
|
<Compile Include="ConvertBase32.cs" />
|
||||||
|
<Compile Include="ArchiveService.cs" />
|
||||||
<Compile Include="Cache\Cached.cs" />
|
<Compile Include="Cache\Cached.cs" />
|
||||||
<Compile Include="Cache\CacheManager.cs" />
|
<Compile Include="Cache\CacheManager.cs" />
|
||||||
<Compile Include="Cache\ICached.cs" />
|
<Compile Include="Cache\ICached.cs" />
|
||||||
|
@ -11,9 +11,9 @@ public static class PathExtensions
|
|||||||
private const string APP_CONFIG_FILE = "config.xml";
|
private const string APP_CONFIG_FILE = "config.xml";
|
||||||
private const string NZBDRONE_DB = "nzbdrone.db";
|
private const string NZBDRONE_DB = "nzbdrone.db";
|
||||||
private const string NZBDRONE_LOG_DB = "logs.db";
|
private const string NZBDRONE_LOG_DB = "logs.db";
|
||||||
private const string BACKUP_ZIP_FILE = "NzbDrone_Backup.zip";
|
|
||||||
private const string NLOG_CONFIG_FILE = "nlog.config";
|
private const string NLOG_CONFIG_FILE = "nlog.config";
|
||||||
private const string UPDATE_CLIENT_EXE = "NzbDrone.Update.exe";
|
private const string UPDATE_CLIENT_EXE = "NzbDrone.Update.exe";
|
||||||
|
private const string BACKUP_FOLDER = "Backups";
|
||||||
|
|
||||||
private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "nzbdrone_update" + Path.DirectorySeparatorChar;
|
private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "nzbdrone_update" + Path.DirectorySeparatorChar;
|
||||||
private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "NzbDrone" + Path.DirectorySeparatorChar;
|
private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "NzbDrone" + Path.DirectorySeparatorChar;
|
||||||
@ -226,9 +226,9 @@ public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
|
|||||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE);
|
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetConfigBackupFile(this IAppFolderInfo appFolderInfo)
|
public static string GetBackupFolder(this IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
return Path.Combine(GetAppDataPath(appFolderInfo), BACKUP_ZIP_FILE);
|
return Path.Combine(GetAppDataPath(appFolderInfo), BACKUP_FOLDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetNzbDroneDatabase(this IAppFolderInfo appFolderInfo)
|
public static string GetNzbDroneDatabase(this IAppFolderInfo appFolderInfo)
|
||||||
|
@ -18,7 +18,7 @@ public class CleanupFixture : CoreTest
|
|||||||
|
|
||||||
private void WithExpired()
|
private void WithExpired()
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.FolderGetLastWrite(It.IsAny<String>()))
|
Mocker.GetMock<IDiskProvider>().Setup(s => s.FolderGetLastWriteUtc(It.IsAny<String>()))
|
||||||
.Returns(DateTime.UtcNow.AddDays(-10));
|
.Returns(DateTime.UtcNow.AddDays(-10));
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.FileGetLastWriteUtc(It.IsAny<String>()))
|
Mocker.GetMock<IDiskProvider>().Setup(s => s.FileGetLastWriteUtc(It.IsAny<String>()))
|
||||||
@ -27,7 +27,7 @@ private void WithExpired()
|
|||||||
|
|
||||||
private void WithNonExpired()
|
private void WithNonExpired()
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.FolderGetLastWrite(It.IsAny<String>()))
|
Mocker.GetMock<IDiskProvider>().Setup(s => s.FolderGetLastWriteUtc(It.IsAny<String>()))
|
||||||
.Returns(DateTime.UtcNow.AddDays(-3));
|
.Returns(DateTime.UtcNow.AddDays(-3));
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.FileGetLastWriteUtc(It.IsAny<String>()))
|
Mocker.GetMock<IDiskProvider>().Setup(s => s.FileGetLastWriteUtc(It.IsAny<String>()))
|
||||||
|
11
src/NzbDrone.Core/Backup/Backup.cs
Normal file
11
src/NzbDrone.Core/Backup/Backup.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Backup
|
||||||
|
{
|
||||||
|
public class Backup
|
||||||
|
{
|
||||||
|
public String Path { get; set; }
|
||||||
|
public BackupType Type { get; set; }
|
||||||
|
public DateTime Time { get; set; }
|
||||||
|
}
|
||||||
|
}
|
24
src/NzbDrone.Core/Backup/BackupCommand.cs
Normal file
24
src/NzbDrone.Core/Backup/BackupCommand.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Backup
|
||||||
|
{
|
||||||
|
public class BackupCommand : Command
|
||||||
|
{
|
||||||
|
public BackupType Type { get; set; }
|
||||||
|
|
||||||
|
public override bool SendUpdatesToClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BackupType
|
||||||
|
{
|
||||||
|
Scheduled = 0 ,
|
||||||
|
Manual = 1,
|
||||||
|
Update = 2
|
||||||
|
}
|
||||||
|
}
|
168
src/NzbDrone.Core/Backup/BackupService.cs
Normal file
168
src/NzbDrone.Core/Backup/BackupService.cs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Marr.Data;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Backup
|
||||||
|
{
|
||||||
|
public interface IBackupService
|
||||||
|
{
|
||||||
|
void Backup(BackupType backupType);
|
||||||
|
List<Backup> GetBackups();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BackupService : IBackupService, IExecute<BackupCommand>
|
||||||
|
{
|
||||||
|
private readonly IDatabase _maindDb;
|
||||||
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
|
private readonly IArchiveService _archiveService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
private string _backupTempFolder;
|
||||||
|
|
||||||
|
private static readonly Regex BackupFileRegex = new Regex(@"nzbdrone_backup_[._0-9]+\.zip", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public BackupService(IDatabase maindDb,
|
||||||
|
IDiskProvider diskProvider,
|
||||||
|
IAppFolderInfo appFolderInfo,
|
||||||
|
IArchiveService archiveService,
|
||||||
|
Logger logger)
|
||||||
|
{
|
||||||
|
_maindDb = maindDb;
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
_appFolderInfo = appFolderInfo;
|
||||||
|
_archiveService = archiveService;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_backupTempFolder = Path.Combine(_appFolderInfo.TempFolder, "nzbdrone_backup");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Backup(BackupType backupType)
|
||||||
|
{
|
||||||
|
_logger.ProgressInfo("Starting Backup");
|
||||||
|
|
||||||
|
var backupFilename = String.Format("nzbdrone_backup_{0:yyyy.MM.dd_HH.mm.ss}.zip", DateTime.Now);
|
||||||
|
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
|
||||||
|
|
||||||
|
Cleanup();
|
||||||
|
|
||||||
|
if (backupType != BackupType.Manual)
|
||||||
|
{
|
||||||
|
CleanupOldBackups(backupType);
|
||||||
|
}
|
||||||
|
|
||||||
|
_diskProvider.EnsureFolder(_backupTempFolder);
|
||||||
|
_diskProvider.EnsureFolder(_appFolderInfo.GetBackupFolder());
|
||||||
|
|
||||||
|
BackupConfigFile();
|
||||||
|
BackupDatabase();
|
||||||
|
|
||||||
|
_logger.ProgressDebug("Creating backup zip");
|
||||||
|
_archiveService.CreateZip(backupPath, _diskProvider.GetFiles(_backupTempFolder, SearchOption.TopDirectoryOnly));
|
||||||
|
_logger.ProgressDebug("Backup zip created");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Backup> GetBackups()
|
||||||
|
{
|
||||||
|
var backups = new List<Backup>();
|
||||||
|
|
||||||
|
foreach (var backupType in Enum.GetValues(typeof(BackupType)).Cast<BackupType>())
|
||||||
|
{
|
||||||
|
var folder = GetBackupFolder(backupType);
|
||||||
|
|
||||||
|
if (_diskProvider.FolderExists(folder))
|
||||||
|
{
|
||||||
|
backups.AddRange(GetBackupFiles(folder).Select(b => new Backup
|
||||||
|
{
|
||||||
|
Path = Path.GetFileName(b),
|
||||||
|
Type = backupType,
|
||||||
|
Time = _diskProvider.FileGetLastWriteUtc(b)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return backups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cleanup()
|
||||||
|
{
|
||||||
|
if (_diskProvider.FolderExists(_backupTempFolder))
|
||||||
|
{
|
||||||
|
_diskProvider.EmptyFolder(_backupTempFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BackupDatabase()
|
||||||
|
{
|
||||||
|
_logger.ProgressDebug("Backing up database");
|
||||||
|
|
||||||
|
using (var unitOfWork = new UnitOfWork(() => _maindDb.GetDataMapper()))
|
||||||
|
{
|
||||||
|
unitOfWork.BeginTransaction();
|
||||||
|
|
||||||
|
var databaseFile = _appFolderInfo.GetNzbDroneDatabase();
|
||||||
|
var tempDatabaseFile = Path.Combine(_backupTempFolder, Path.GetFileName(databaseFile));
|
||||||
|
|
||||||
|
_diskProvider.CopyFile(databaseFile, tempDatabaseFile, true);
|
||||||
|
|
||||||
|
unitOfWork.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BackupConfigFile()
|
||||||
|
{
|
||||||
|
_logger.ProgressDebug("Backing up config.xml");
|
||||||
|
|
||||||
|
var configFile = _appFolderInfo.GetConfigPath();
|
||||||
|
var tempConfigFile = Path.Combine(_backupTempFolder, Path.GetFileName(configFile));
|
||||||
|
|
||||||
|
_diskProvider.CopyFile(configFile, tempConfigFile, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupOldBackups(BackupType backupType)
|
||||||
|
{
|
||||||
|
_logger.Debug("Cleaning up old backup files");
|
||||||
|
var files = GetBackupFiles(GetBackupFolder(backupType));
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var lastWriteTime = _diskProvider.FileGetLastWriteUtc(file);
|
||||||
|
|
||||||
|
if (lastWriteTime.AddDays(28) < DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
_logger.Debug("Deleting old backup file: {0}", file);
|
||||||
|
_diskProvider.DeleteFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
var files = _diskProvider.GetFiles(path, SearchOption.TopDirectoryOnly);
|
||||||
|
|
||||||
|
return files.Where(f => BackupFileRegex.IsMatch(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(BackupCommand message)
|
||||||
|
{
|
||||||
|
Backup(message.Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Core.Backup;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Configuration.Events;
|
using NzbDrone.Core.Configuration.Events;
|
||||||
using NzbDrone.Core.DataAugmentation.Scene;
|
using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
@ -56,6 +57,7 @@ public void Handle(ApplicationStartedEvent message)
|
|||||||
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
|
new ScheduledTask
|
||||||
{
|
{
|
||||||
|
@ -128,7 +128,7 @@ public void Cleanup()
|
|||||||
|
|
||||||
foreach (var folder in _diskProvider.GetDirectories(_configService.RecycleBin))
|
foreach (var folder in _diskProvider.GetDirectories(_configService.RecycleBin))
|
||||||
{
|
{
|
||||||
if (_diskProvider.FolderGetLastWrite(folder).AddDays(7) > DateTime.UtcNow)
|
if (_diskProvider.FolderGetLastWriteUtc(folder).AddDays(7) > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
logger.Debug("Folder hasn't expired yet, skipping: {0}", folder);
|
logger.Debug("Folder hasn't expired yet, skipping: {0}", folder);
|
||||||
continue;
|
continue;
|
||||||
|
@ -104,6 +104,9 @@
|
|||||||
<Link>Properties\SharedAssemblyInfo.cs</Link>
|
<Link>Properties\SharedAssemblyInfo.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Annotations\FieldDefinitionAttribute.cs" />
|
<Compile Include="Annotations\FieldDefinitionAttribute.cs" />
|
||||||
|
<Compile Include="Backup\Backup.cs" />
|
||||||
|
<Compile Include="Backup\BackupCommand.cs" />
|
||||||
|
<Compile Include="Backup\BackupService.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" />
|
||||||
|
@ -168,3 +168,7 @@ td.delete-episode-file-cell {
|
|||||||
.episode-number-cell {
|
.episode-number-cell {
|
||||||
cursor : default;
|
cursor : default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backup-type-cell {
|
||||||
|
width : 20px;
|
||||||
|
}
|
||||||
|
@ -95,6 +95,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
|
cursor : default;
|
||||||
|
|
||||||
&.sortable {
|
&.sortable {
|
||||||
&:hover {
|
&:hover {
|
||||||
background : @table-bg-hover;
|
background : @table-bg-hover;
|
||||||
|
@ -12,7 +12,6 @@ define(
|
|||||||
'Release/ReleaseLayout',
|
'Release/ReleaseLayout',
|
||||||
'System/SystemLayout',
|
'System/SystemLayout',
|
||||||
'SeasonPass/SeasonPassLayout',
|
'SeasonPass/SeasonPassLayout',
|
||||||
'System/Update/UpdateLayout',
|
|
||||||
'Series/Editor/SeriesEditorLayout'
|
'Series/Editor/SeriesEditorLayout'
|
||||||
], function (NzbDroneController,
|
], function (NzbDroneController,
|
||||||
AppLayout,
|
AppLayout,
|
||||||
@ -25,7 +24,6 @@ define(
|
|||||||
ReleaseLayout,
|
ReleaseLayout,
|
||||||
SystemLayout,
|
SystemLayout,
|
||||||
SeasonPassLayout,
|
SeasonPassLayout,
|
||||||
UpdateLayout,
|
|
||||||
SeriesEditorLayout) {
|
SeriesEditorLayout) {
|
||||||
return NzbDroneController.extend({
|
return NzbDroneController.extend({
|
||||||
|
|
||||||
@ -71,11 +69,6 @@ define(
|
|||||||
this.showMainRegion(new SeasonPassLayout());
|
this.showMainRegion(new SeasonPassLayout());
|
||||||
},
|
},
|
||||||
|
|
||||||
update: function () {
|
|
||||||
this.setTitle('Updates');
|
|
||||||
this.showMainRegion(new UpdateLayout());
|
|
||||||
},
|
|
||||||
|
|
||||||
seriesEditor: function () {
|
seriesEditor: function () {
|
||||||
this.setTitle('Series Editor');
|
this.setTitle('Series Editor');
|
||||||
this.showMainRegion(new SeriesEditorLayout());
|
this.showMainRegion(new SeriesEditorLayout());
|
||||||
|
19
src/UI/System/Backup/BackupCollection.js
Normal file
19
src/UI/System/Backup/BackupCollection.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'backbone.pageable',
|
||||||
|
'System/Backup/BackupModel'
|
||||||
|
], function (PageableCollection, BackupModel) {
|
||||||
|
return PageableCollection.extend({
|
||||||
|
url : window.NzbDrone.ApiRoot + '/system/backup',
|
||||||
|
model: BackupModel,
|
||||||
|
|
||||||
|
state: {
|
||||||
|
sortKey : 'time',
|
||||||
|
order : 1,
|
||||||
|
pageSize : 100000
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: 'client'
|
||||||
|
});
|
||||||
|
});
|
10
src/UI/System/Backup/BackupEmptyView.js
Normal file
10
src/UI/System/Backup/BackupEmptyView.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'marionette'
|
||||||
|
], function (Marionette) {
|
||||||
|
return Marionette.ItemView.extend({
|
||||||
|
template: 'System/Backup/BackupEmptyViewTemplate'
|
||||||
|
});
|
||||||
|
});
|
1
src/UI/System/Backup/BackupEmptyViewTemplate.html
Normal file
1
src/UI/System/Backup/BackupEmptyViewTemplate.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div>No backups are available</div>
|
12
src/UI/System/Backup/BackupFilenameCell.js
Normal file
12
src/UI/System/Backup/BackupFilenameCell.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
'use strict';
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'Cells/TemplatedCell'
|
||||||
|
], function (TemplatedCell) {
|
||||||
|
return TemplatedCell.extend({
|
||||||
|
|
||||||
|
className: 'series-title',
|
||||||
|
template : 'System/Backup/BackupFilenameCellTemplate'
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
2
src/UI/System/Backup/BackupFilenameCellTemplate.html
Normal file
2
src/UI/System/Backup/BackupFilenameCellTemplate.html
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<a href="{{urlBack}}/backup/{{type}}/{{name}}" class="no-router">{{name}}</a>
|
||||||
|
|
106
src/UI/System/Backup/BackupLayout.js
Normal file
106
src/UI/System/Backup/BackupLayout.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
'use strict';
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'vent',
|
||||||
|
'marionette',
|
||||||
|
'backgrid',
|
||||||
|
'System/Backup/BackupCollection',
|
||||||
|
'Cells/RelativeDateCell',
|
||||||
|
'System/Backup/BackupFilenameCell',
|
||||||
|
'System/Backup/BackupTypeCell',
|
||||||
|
'System/Backup/BackupEmptyView',
|
||||||
|
'Shared/LoadingView',
|
||||||
|
'Shared/Toolbar/ToolbarLayout'
|
||||||
|
], function (vent, Marionette, Backgrid, BackupCollection, RelativeDateCell, BackupFilenameCell, BackupTypeCell, EmptyView, LoadingView, ToolbarLayout) {
|
||||||
|
return Marionette.Layout.extend({
|
||||||
|
template: 'System/Backup/BackupLayoutTemplate',
|
||||||
|
|
||||||
|
regions: {
|
||||||
|
backups : '#x-backups',
|
||||||
|
toolbar : '#x-backup-toolbar'
|
||||||
|
},
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name : 'type',
|
||||||
|
label : '',
|
||||||
|
sortable : false,
|
||||||
|
cell : BackupTypeCell
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'this',
|
||||||
|
label : 'Name',
|
||||||
|
sortable : false,
|
||||||
|
cell : BackupFilenameCell
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'time',
|
||||||
|
label : 'Time',
|
||||||
|
sortable : false,
|
||||||
|
cell : RelativeDateCell
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
leftSideButtons: {
|
||||||
|
type : 'default',
|
||||||
|
storeState: false,
|
||||||
|
collapse : false,
|
||||||
|
items :
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title : 'Backup',
|
||||||
|
icon : 'icon-file-text',
|
||||||
|
command : 'backup',
|
||||||
|
properties : { type: 'manual' },
|
||||||
|
successMessage: 'Database and settings were backed up successfully',
|
||||||
|
errorMessage : 'Backup Failed!'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
this.backupCollection = new BackupCollection();
|
||||||
|
|
||||||
|
this.listenTo(this.backupCollection, 'sync', this._showBackups);
|
||||||
|
this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete);
|
||||||
|
},
|
||||||
|
|
||||||
|
onRender: function () {
|
||||||
|
this._showToolbar();
|
||||||
|
this.backups.show(new LoadingView());
|
||||||
|
|
||||||
|
this.backupCollection.fetch();
|
||||||
|
},
|
||||||
|
|
||||||
|
_showBackups: function () {
|
||||||
|
|
||||||
|
if (this.backupCollection.length === 0) {
|
||||||
|
this.backups.show(new EmptyView());
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
this.backups.show(new Backgrid.Grid({
|
||||||
|
columns : this.columns,
|
||||||
|
collection: this.backupCollection,
|
||||||
|
className : 'table table-hover'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_showToolbar : function () {
|
||||||
|
this.toolbar.show(new ToolbarLayout({
|
||||||
|
left :
|
||||||
|
[
|
||||||
|
this.leftSideButtons
|
||||||
|
],
|
||||||
|
context: this
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
_commandComplete: function (options) {
|
||||||
|
if (options.command.get('name') === 'backup') {
|
||||||
|
this.backupCollection.fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
10
src/UI/System/Backup/BackupLayoutTemplate.html
Normal file
10
src/UI/System/Backup/BackupLayoutTemplate.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div id="x-backup-toolbar"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div id="x-backups" class="table-responsive"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
9
src/UI/System/Backup/BackupModel.js
Normal file
9
src/UI/System/Backup/BackupModel.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'backbone'
|
||||||
|
], function (Backbone) {
|
||||||
|
return Backbone.Model.extend({
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
33
src/UI/System/Backup/BackupTypeCell.js
Normal file
33
src/UI/System/Backup/BackupTypeCell.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
'use strict';
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'Cells/NzbDroneCell'
|
||||||
|
], function (NzbDroneCell) {
|
||||||
|
return NzbDroneCell.extend({
|
||||||
|
|
||||||
|
className: 'backup-type-cell',
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
this.$el.empty();
|
||||||
|
|
||||||
|
var icon = 'icon-time';
|
||||||
|
var title = 'Scheduled';
|
||||||
|
|
||||||
|
var type = this.model.get(this.column.get('name'));
|
||||||
|
|
||||||
|
if (type === 'manual') {
|
||||||
|
icon = 'icon-book';
|
||||||
|
title = 'Manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (type === 'update') {
|
||||||
|
icon = 'icon-retweet';
|
||||||
|
title = 'Before update';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title));
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -7,6 +7,7 @@ define(
|
|||||||
'System/Info/SystemInfoLayout',
|
'System/Info/SystemInfoLayout',
|
||||||
'System/Logs/LogsLayout',
|
'System/Logs/LogsLayout',
|
||||||
'System/Update/UpdateLayout',
|
'System/Update/UpdateLayout',
|
||||||
|
'System/Backup/BackupLayout',
|
||||||
'Shared/Messenger'
|
'Shared/Messenger'
|
||||||
], function ($,
|
], function ($,
|
||||||
Backbone,
|
Backbone,
|
||||||
@ -14,6 +15,7 @@ define(
|
|||||||
SystemInfoLayout,
|
SystemInfoLayout,
|
||||||
LogsLayout,
|
LogsLayout,
|
||||||
UpdateLayout,
|
UpdateLayout,
|
||||||
|
BackupLayout,
|
||||||
Messenger) {
|
Messenger) {
|
||||||
return Marionette.Layout.extend({
|
return Marionette.Layout.extend({
|
||||||
template: 'System/SystemLayoutTemplate',
|
template: 'System/SystemLayoutTemplate',
|
||||||
@ -21,19 +23,22 @@ define(
|
|||||||
regions: {
|
regions: {
|
||||||
info : '#info',
|
info : '#info',
|
||||||
logs : '#logs',
|
logs : '#logs',
|
||||||
updates: '#updates'
|
updates : '#updates',
|
||||||
|
backup : '#backup'
|
||||||
},
|
},
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
infoTab : '.x-info-tab',
|
infoTab : '.x-info-tab',
|
||||||
logsTab : '.x-logs-tab',
|
logsTab : '.x-logs-tab',
|
||||||
updatesTab: '.x-updates-tab'
|
updatesTab : '.x-updates-tab',
|
||||||
|
backupTab : '.x-backup-tab'
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
'click .x-info-tab' : '_showInfo',
|
'click .x-info-tab' : '_showInfo',
|
||||||
'click .x-logs-tab' : '_showLogs',
|
'click .x-logs-tab' : '_showLogs',
|
||||||
'click .x-updates-tab': '_showUpdates',
|
'click .x-updates-tab': '_showUpdates',
|
||||||
|
'click .x-backup-tab': '_showBackup',
|
||||||
'click .x-shutdown' : '_shutdown',
|
'click .x-shutdown' : '_shutdown',
|
||||||
'click .x-restart' : '_restart'
|
'click .x-restart' : '_restart'
|
||||||
},
|
},
|
||||||
@ -52,6 +57,9 @@ define(
|
|||||||
case 'updates':
|
case 'updates':
|
||||||
this._showUpdates();
|
this._showUpdates();
|
||||||
break;
|
break;
|
||||||
|
case 'backup':
|
||||||
|
this._showBackup();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this._showInfo();
|
this._showInfo();
|
||||||
}
|
}
|
||||||
@ -91,6 +99,16 @@ define(
|
|||||||
this._navigate('system/updates');
|
this._navigate('system/updates');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_showBackup: function (e) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.backup.show(new BackupLayout());
|
||||||
|
this.ui.backupTab.tab('show');
|
||||||
|
this._navigate('system/backup');
|
||||||
|
},
|
||||||
|
|
||||||
_shutdown: function () {
|
_shutdown: function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.NzbDrone.ApiRoot + '/system/shutdown',
|
url: window.NzbDrone.ApiRoot + '/system/shutdown',
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<li><a href="#info" class="x-info-tab no-router">Info</a></li>
|
<li><a href="#info" class="x-info-tab no-router">Info</a></li>
|
||||||
<li><a href="#logs" class="x-logs-tab no-router">Logs</a></li>
|
<li><a href="#logs" class="x-logs-tab no-router">Logs</a></li>
|
||||||
<li><a href="#updates" class="x-updates-tab no-router">Updates</a></li>
|
<li><a href="#updates" class="x-updates-tab no-router">Updates</a></li>
|
||||||
|
<li><a href="#backup" class="x-backup-tab no-router">Backup</a></li>
|
||||||
<li class="lifecycle-controls pull-right">
|
<li class="lifecycle-controls pull-right">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-default btn-icon-only x-shutdown" title="Shutdown" data-container="body">
|
<button class="btn btn-default btn-icon-only x-shutdown" title="Shutdown" data-container="body">
|
||||||
@ -18,4 +19,5 @@
|
|||||||
<div class="tab-pane" id="info"></div>
|
<div class="tab-pane" id="info"></div>
|
||||||
<div class="tab-pane" id="logs"></div>
|
<div class="tab-pane" id="logs"></div>
|
||||||
<div class="tab-pane" id="updates"></div>
|
<div class="tab-pane" id="updates"></div>
|
||||||
|
<div class="tab-pane" id="backup"></div>
|
||||||
</div>
|
</div>
|
Loading…
Reference in New Issue
Block a user