From c5bd8b27fbba8ff357e01c4adef1ec7ab2ec7df1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 30 May 2014 10:34:38 -0700 Subject: [PATCH] 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 --- .gitignore | 1 - .../Frontend/Mappers/BackupFileMapper.cs | 31 ++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 3 + .../System/Backup/BackupModule.cs | 32 ++++ .../System/Backup/BackupResource.cs | 14 ++ .../DiskProviderFixtureBase.cs | 8 +- .../{ArchiveProvider.cs => ArchiveService.cs} | 19 +- src/NzbDrone.Common/Disk/DiskProviderBase.cs | 2 +- src/NzbDrone.Common/Disk/IDiskProvider.cs | 2 +- src/NzbDrone.Common/NzbDrone.Common.csproj | 2 +- src/NzbDrone.Common/PathExtensions.cs | 6 +- .../RecycleBinProviderTests/CleanupFixture.cs | 4 +- src/NzbDrone.Core/Backup/Backup.cs | 11 ++ src/NzbDrone.Core/Backup/BackupCommand.cs | 24 +++ src/NzbDrone.Core/Backup/BackupService.cs | 168 ++++++++++++++++++ src/NzbDrone.Core/Jobs/TaskManager.cs | 2 + .../MediaFiles/RecycleBinProvider.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 + src/UI/Cells/cells.less | 4 + src/UI/Content/theme.less | 2 + src/UI/Controller.js | 7 - src/UI/System/Backup/BackupCollection.js | 19 ++ src/UI/System/Backup/BackupEmptyView.js | 10 ++ .../Backup/BackupEmptyViewTemplate.html | 1 + src/UI/System/Backup/BackupFilenameCell.js | 12 ++ .../Backup/BackupFilenameCellTemplate.html | 2 + src/UI/System/Backup/BackupLayout.js | 106 +++++++++++ .../System/Backup/BackupLayoutTemplate.html | 10 ++ src/UI/System/Backup/BackupModel.js | 9 + src/UI/System/Backup/BackupTypeCell.js | 33 ++++ src/UI/System/SystemLayout.js | 28 ++- src/UI/System/SystemLayoutTemplate.html | 2 + 32 files changed, 552 insertions(+), 27 deletions(-) create mode 100644 src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs create mode 100644 src/NzbDrone.Api/System/Backup/BackupModule.cs create mode 100644 src/NzbDrone.Api/System/Backup/BackupResource.cs rename src/NzbDrone.Common/{ArchiveProvider.cs => ArchiveService.cs} (86%) create mode 100644 src/NzbDrone.Core/Backup/Backup.cs create mode 100644 src/NzbDrone.Core/Backup/BackupCommand.cs create mode 100644 src/NzbDrone.Core/Backup/BackupService.cs create mode 100644 src/UI/System/Backup/BackupCollection.js create mode 100644 src/UI/System/Backup/BackupEmptyView.js create mode 100644 src/UI/System/Backup/BackupEmptyViewTemplate.html create mode 100644 src/UI/System/Backup/BackupFilenameCell.js create mode 100644 src/UI/System/Backup/BackupFilenameCellTemplate.html create mode 100644 src/UI/System/Backup/BackupLayout.js create mode 100644 src/UI/System/Backup/BackupLayoutTemplate.html create mode 100644 src/UI/System/Backup/BackupModel.js create mode 100644 src/UI/System/Backup/BackupTypeCell.js diff --git a/.gitignore b/.gitignore index d1a9a9a44..d537ab816 100644 --- a/.gitignore +++ b/.gitignore @@ -84,7 +84,6 @@ Generated_Code #added for RIA/Silverlight projects # Backup & report files from converting an old project file to a newer # Visual Studio version. Backup files are not needed, because we have git ;-) _UpgradeReport_Files/ -Backup*/ UpgradeLog*.XML # SQL Server files diff --git a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs new file mode 100644 index 000000000..a2e111430 --- /dev/null +++ b/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index f0ab8561f..d4d52cb96 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -131,6 +131,7 @@ + @@ -165,6 +166,8 @@ + + diff --git a/src/NzbDrone.Api/System/Backup/BackupModule.cs b/src/NzbDrone.Api/System/Backup/BackupModule.cs new file mode 100644 index 000000000..b5074793e --- /dev/null +++ b/src/NzbDrone.Api/System/Backup/BackupModule.cs @@ -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 + { + private readonly IBackupService _backupService; + + public BackupModule(IBackupService backupService) : base("system/backup") + { + _backupService = backupService; + GetResourceAll = GetBackupFiles; + } + + public List 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(); + } + } +} diff --git a/src/NzbDrone.Api/System/Backup/BackupResource.cs b/src/NzbDrone.Api/System/Backup/BackupResource.cs new file mode 100644 index 000000000..732eee3c0 --- /dev/null +++ b/src/NzbDrone.Api/System/Backup/BackupResource.cs @@ -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; } + } +} diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs index 5af9890ba..493914c12 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs @@ -172,7 +172,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests public void empty_folder_should_return_folder_modified_date() { var tempfolder = new DirectoryInfo(TempFolder); - Subject.FolderGetLastWrite(TempFolder).Should().Be(tempfolder.LastWriteTimeUtc); + Subject.FolderGetLastWriteUtc(TempFolder).Should().Be(tempfolder.LastWriteTimeUtc); } [Test] @@ -189,8 +189,8 @@ namespace NzbDrone.Common.Test.DiskProviderTests Subject.WriteAllText(testFile, "Test"); - Subject.FolderGetLastWrite(TempFolder).Should().BeOnOrAfter(DateTime.UtcNow.AddMinutes(-1)); - Subject.FolderGetLastWrite(TempFolder).Should().BeBefore(DateTime.UtcNow.AddMinutes(1)); + Subject.FolderGetLastWriteUtc(TempFolder).Should().BeOnOrAfter(DateTime.UtcNow.AddMinutes(-1)); + Subject.FolderGetLastWriteUtc(TempFolder).Should().BeBefore(DateTime.UtcNow.AddMinutes(1)); } [Test] @@ -238,7 +238,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests [Explicit] public void check_last_write() { - Console.WriteLine(Subject.FolderGetLastWrite(GetFilledTempFolder().FullName)); + Console.WriteLine(Subject.FolderGetLastWriteUtc(GetFilledTempFolder().FullName)); Console.WriteLine(GetFilledTempFolder().LastWriteTimeUtc); } diff --git a/src/NzbDrone.Common/ArchiveProvider.cs b/src/NzbDrone.Common/ArchiveService.cs similarity index 86% rename from src/NzbDrone.Common/ArchiveProvider.cs rename to src/NzbDrone.Common/ArchiveService.cs index 5d7d644d5..1899d8f3d 100644 --- a/src/NzbDrone.Common/ArchiveProvider.cs +++ b/src/NzbDrone.Common/ArchiveService.cs @@ -12,6 +12,8 @@ namespace NzbDrone.Common public interface IArchiveService { void Extract(string compressedFile, string destination); + void ExtractZip(string compressedFile, string destination); + void CreateZip(string path, params string[] files); } public class ArchiveService : IArchiveService @@ -40,7 +42,22 @@ namespace NzbDrone.Common _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)) { diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index c9de8d11c..540c694cd 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Common.Disk return new DirectoryInfo(path).CreationTimeUtc; } - public DateTime FolderGetLastWrite(string path) + public DateTime FolderGetLastWriteUtc(string path) { CheckFolderExists(path); diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index afbd7ce60..473a0211d 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Common.Disk void SetPermissions(string path, string mask, string user, string group); long? GetTotalSize(string path); DateTime FolderGetCreationTimeUtc(string path); - DateTime FolderGetLastWrite(string path); + DateTime FolderGetLastWriteUtc(string path); DateTime FileGetCreationTimeUtc(string path); DateTime FileGetLastWrite(string path); DateTime FileGetLastWriteUtc(string path); diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index bab538ec7..e9bdd86f0 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -59,8 +59,8 @@ - + diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index f57051f61..b99d39a69 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -11,9 +11,9 @@ namespace NzbDrone.Common private const string APP_CONFIG_FILE = "config.xml"; private const string NZBDRONE_DB = "nzbdrone.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 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_PACKAGE_FOLDER_NAME = "NzbDrone" + Path.DirectorySeparatorChar; @@ -226,9 +226,9 @@ namespace NzbDrone.Common 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) diff --git a/src/NzbDrone.Core.Test/ProviderTests/RecycleBinProviderTests/CleanupFixture.cs b/src/NzbDrone.Core.Test/ProviderTests/RecycleBinProviderTests/CleanupFixture.cs index 9436770ae..2f92eae2a 100644 --- a/src/NzbDrone.Core.Test/ProviderTests/RecycleBinProviderTests/CleanupFixture.cs +++ b/src/NzbDrone.Core.Test/ProviderTests/RecycleBinProviderTests/CleanupFixture.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests private void WithExpired() { - Mocker.GetMock().Setup(s => s.FolderGetLastWrite(It.IsAny())) + Mocker.GetMock().Setup(s => s.FolderGetLastWriteUtc(It.IsAny())) .Returns(DateTime.UtcNow.AddDays(-10)); Mocker.GetMock().Setup(s => s.FileGetLastWriteUtc(It.IsAny())) @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests private void WithNonExpired() { - Mocker.GetMock().Setup(s => s.FolderGetLastWrite(It.IsAny())) + Mocker.GetMock().Setup(s => s.FolderGetLastWriteUtc(It.IsAny())) .Returns(DateTime.UtcNow.AddDays(-3)); Mocker.GetMock().Setup(s => s.FileGetLastWriteUtc(It.IsAny())) diff --git a/src/NzbDrone.Core/Backup/Backup.cs b/src/NzbDrone.Core/Backup/Backup.cs new file mode 100644 index 000000000..880ef6106 --- /dev/null +++ b/src/NzbDrone.Core/Backup/Backup.cs @@ -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; } + } +} diff --git a/src/NzbDrone.Core/Backup/BackupCommand.cs b/src/NzbDrone.Core/Backup/BackupCommand.cs new file mode 100644 index 000000000..29199c67a --- /dev/null +++ b/src/NzbDrone.Core/Backup/BackupCommand.cs @@ -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 + } +} diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs new file mode 100644 index 000000000..eb90fa3d2 --- /dev/null +++ b/src/NzbDrone.Core/Backup/BackupService.cs @@ -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 GetBackups(); + } + + public class BackupService : IBackupService, IExecute + { + 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 GetBackups() + { + var backups = new List(); + + foreach (var backupType in Enum.GetValues(typeof(BackupType)).Cast()) + { + 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 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); + } + } +} diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 788e6df0c..4656f3a76 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Core.Backup; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.DataAugmentation.Scene; @@ -56,6 +57,7 @@ namespace NzbDrone.Core.Jobs new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, + new ScheduledTask{ Interval = 7*24*60, TypeName = typeof(BackupCommand).FullName}, new ScheduledTask { diff --git a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs index ac4fa3c3d..b8fad1a47 100644 --- a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs +++ b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs @@ -128,7 +128,7 @@ namespace NzbDrone.Core.MediaFiles 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); continue; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9b758ab25..9e4e64c3f 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -104,6 +104,9 @@ Properties\SharedAssemblyInfo.cs + + + diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 8d591460b..98c5fbf3a 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -168,3 +168,7 @@ td.delete-episode-file-cell { .episode-number-cell { cursor : default; } + +.backup-type-cell { + width : 20px; +} diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 47a0b323f..6c13b071b 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -95,6 +95,8 @@ } th { + cursor : default; + &.sortable { &:hover { background : @table-bg-hover; diff --git a/src/UI/Controller.js b/src/UI/Controller.js index 19d7c3760..87ef4e535 100644 --- a/src/UI/Controller.js +++ b/src/UI/Controller.js @@ -12,7 +12,6 @@ define( 'Release/ReleaseLayout', 'System/SystemLayout', 'SeasonPass/SeasonPassLayout', - 'System/Update/UpdateLayout', 'Series/Editor/SeriesEditorLayout' ], function (NzbDroneController, AppLayout, @@ -25,7 +24,6 @@ define( ReleaseLayout, SystemLayout, SeasonPassLayout, - UpdateLayout, SeriesEditorLayout) { return NzbDroneController.extend({ @@ -71,11 +69,6 @@ define( this.showMainRegion(new SeasonPassLayout()); }, - update: function () { - this.setTitle('Updates'); - this.showMainRegion(new UpdateLayout()); - }, - seriesEditor: function () { this.setTitle('Series Editor'); this.showMainRegion(new SeriesEditorLayout()); diff --git a/src/UI/System/Backup/BackupCollection.js b/src/UI/System/Backup/BackupCollection.js new file mode 100644 index 000000000..6b52ca3b9 --- /dev/null +++ b/src/UI/System/Backup/BackupCollection.js @@ -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' + }); + }); diff --git a/src/UI/System/Backup/BackupEmptyView.js b/src/UI/System/Backup/BackupEmptyView.js new file mode 100644 index 000000000..51e72eafd --- /dev/null +++ b/src/UI/System/Backup/BackupEmptyView.js @@ -0,0 +1,10 @@ +'use strict'; + +define( + [ + 'marionette' + ], function (Marionette) { + return Marionette.ItemView.extend({ + template: 'System/Backup/BackupEmptyViewTemplate' + }); + }); diff --git a/src/UI/System/Backup/BackupEmptyViewTemplate.html b/src/UI/System/Backup/BackupEmptyViewTemplate.html new file mode 100644 index 000000000..5a480c62a --- /dev/null +++ b/src/UI/System/Backup/BackupEmptyViewTemplate.html @@ -0,0 +1 @@ +
No backups are available
\ No newline at end of file diff --git a/src/UI/System/Backup/BackupFilenameCell.js b/src/UI/System/Backup/BackupFilenameCell.js new file mode 100644 index 000000000..f853bdf6e --- /dev/null +++ b/src/UI/System/Backup/BackupFilenameCell.js @@ -0,0 +1,12 @@ +'use strict'; +define( + [ + 'Cells/TemplatedCell' + ], function (TemplatedCell) { + return TemplatedCell.extend({ + + className: 'series-title', + template : 'System/Backup/BackupFilenameCellTemplate' + + }); + }); diff --git a/src/UI/System/Backup/BackupFilenameCellTemplate.html b/src/UI/System/Backup/BackupFilenameCellTemplate.html new file mode 100644 index 000000000..9dc32e9f5 --- /dev/null +++ b/src/UI/System/Backup/BackupFilenameCellTemplate.html @@ -0,0 +1,2 @@ +{{name}} + diff --git a/src/UI/System/Backup/BackupLayout.js b/src/UI/System/Backup/BackupLayout.js new file mode 100644 index 000000000..98840384d --- /dev/null +++ b/src/UI/System/Backup/BackupLayout.js @@ -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(); + } + } + }); + }); diff --git a/src/UI/System/Backup/BackupLayoutTemplate.html b/src/UI/System/Backup/BackupLayoutTemplate.html new file mode 100644 index 000000000..b2b9f91f9 --- /dev/null +++ b/src/UI/System/Backup/BackupLayoutTemplate.html @@ -0,0 +1,10 @@ +
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/System/Backup/BackupModel.js b/src/UI/System/Backup/BackupModel.js new file mode 100644 index 000000000..530a080c6 --- /dev/null +++ b/src/UI/System/Backup/BackupModel.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + + }); + }); diff --git a/src/UI/System/Backup/BackupTypeCell.js b/src/UI/System/Backup/BackupTypeCell.js new file mode 100644 index 000000000..d836299bb --- /dev/null +++ b/src/UI/System/Backup/BackupTypeCell.js @@ -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(''.format(icon, title)); + + return this; + } + }); + }); diff --git a/src/UI/System/SystemLayout.js b/src/UI/System/SystemLayout.js index 3500681c7..29e8562f7 100644 --- a/src/UI/System/SystemLayout.js +++ b/src/UI/System/SystemLayout.js @@ -7,6 +7,7 @@ define( 'System/Info/SystemInfoLayout', 'System/Logs/LogsLayout', 'System/Update/UpdateLayout', + 'System/Backup/BackupLayout', 'Shared/Messenger' ], function ($, Backbone, @@ -14,26 +15,30 @@ define( SystemInfoLayout, LogsLayout, UpdateLayout, + BackupLayout, Messenger) { return Marionette.Layout.extend({ template: 'System/SystemLayoutTemplate', regions: { - info : '#info', + info : '#info', logs : '#logs', - updates: '#updates' + updates : '#updates', + backup : '#backup' }, ui: { - infoTab : '.x-info-tab', - logsTab : '.x-logs-tab', - updatesTab: '.x-updates-tab' + infoTab : '.x-info-tab', + logsTab : '.x-logs-tab', + updatesTab : '.x-updates-tab', + backupTab : '.x-backup-tab' }, events: { 'click .x-info-tab' : '_showInfo', 'click .x-logs-tab' : '_showLogs', 'click .x-updates-tab': '_showUpdates', + 'click .x-backup-tab': '_showBackup', 'click .x-shutdown' : '_shutdown', 'click .x-restart' : '_restart' }, @@ -52,6 +57,9 @@ define( case 'updates': this._showUpdates(); break; + case 'backup': + this._showBackup(); + break; default: this._showInfo(); } @@ -91,6 +99,16 @@ define( 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 () { $.ajax({ url: window.NzbDrone.ApiRoot + '/system/shutdown', diff --git a/src/UI/System/SystemLayoutTemplate.html b/src/UI/System/SystemLayoutTemplate.html index e3b245715..aa6996850 100644 --- a/src/UI/System/SystemLayoutTemplate.html +++ b/src/UI/System/SystemLayoutTemplate.html @@ -2,6 +2,7 @@
  • Info
  • Logs
  • Updates
  • +
  • Backup
  • \ No newline at end of file