From 49168cad25f0bda75998db4e78032eb95c991313 Mon Sep 17 00:00:00 2001
From: Mark McDowall <markus.mcd5@gmail.com>
Date: Sun, 26 Jan 2014 00:57:14 -0800
Subject: [PATCH] UI and opt-in for setting permissions

---
 .../Configuration/ConfigService.cs            |  9 ++-
 .../Configuration/IConfigService.cs           |  1 +
 .../MediaFiles/EpisodeFileMovingService.cs    | 20 ++++++-
 .../MediaFiles/SetMediaFilePermissions.cs     | 60 -------------------
 src/NzbDrone.Core/NzbDrone.Core.csproj        |  1 -
 src/NzbDrone.Mono/DiskProvider.cs             |  7 +++
 src/UI/Handlebars/Helpers/Os.js               |  9 +++
 .../backbone.marionette.templates.js          |  1 +
 .../MediaManagement/MediaManagementLayout.js  |  9 ++-
 .../MediaManagementLayoutTemplate.html        |  1 +
 .../Permissions/PermissionsView.js            | 39 ++++++++++++
 .../Permissions/PermissionsViewTemplate.html  | 48 +++++++++++++++
 12 files changed, 137 insertions(+), 68 deletions(-)
 delete mode 100644 src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs
 create mode 100644 src/UI/Handlebars/Helpers/Os.js
 create mode 100644 src/UI/Settings/MediaManagement/Permissions/PermissionsView.js
 create mode 100644 src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html

diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs
index 1427f998f..17b5eba03 100644
--- a/src/NzbDrone.Core/Configuration/ConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/ConfigService.cs
@@ -284,9 +284,16 @@ namespace NzbDrone.Core.Configuration
             set { SetValue("DownloadClientWorkingFolders", value); }
         }
 
+        public Boolean SetPermissionsLinux
+        {
+            get { return GetValueBoolean("SetPermissionsLinux", false); }
+
+            set { SetValue("SetPermissionsLinux", value); }
+        }
+
         public String FileChmod
         {
-            get { return GetValue("FileChmod", "0755"); }
+            get { return GetValue("FileChmod", "0644"); }
 
             set { SetValue("FileChmod", value); }
         }
diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs
index 70982dedd..d70d36def 100644
--- a/src/NzbDrone.Core/Configuration/IConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/IConfigService.cs
@@ -42,6 +42,7 @@ namespace NzbDrone.Core.Configuration
         Boolean EnableFailedDownloadHandling { get; set; }
         Boolean CreateEmptySeriesFolders { get; set; }
         void SaveValues(Dictionary<string, object> configValues);
+        Boolean SetPermissionsLinux { get; set; }
         String FileChmod { get; set; }
         String FolderChmod { get; set; }
     }
diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs
index 626cdb020..a7921453b 100644
--- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs
@@ -79,7 +79,18 @@ namespace NzbDrone.Core.MediaFiles
                 throw new SameFilenameException("File not moved, source and destination are the same", episodeFile.Path);
             }
 
-            _diskProvider.CreateFolder(new FileInfo(destinationFilename).DirectoryName);
+            var directoryName = new FileInfo(destinationFilename).DirectoryName;
+
+            if (_diskProvider.FolderExists(directoryName))
+            {
+                _diskProvider.CreateFolder(directoryName);
+                SetFolderPermissions(directoryName);
+
+                if (!directoryName.PathEquals(series.Path))
+                {
+                    SetFolderPermissions(series.Path);
+                }
+            }
 
             _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename);
             _diskProvider.MoveFile(episodeFile.Path, destinationFilename);
@@ -88,7 +99,6 @@ namespace NzbDrone.Core.MediaFiles
             {
                 _logger.Trace("Setting last write time on series folder: {0}", series.Path);
                 _diskProvider.SetFolderWriteTime(series.Path, episodeFile.DateAdded);
-                SetFolderPermissions(series.Path);
 
                 if (series.SeasonFolder)
                 {
@@ -96,7 +106,6 @@ namespace NzbDrone.Core.MediaFiles
 
                     _logger.Trace("Setting last write time on season folder: {0}", seasonFolder);
                     _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded);
-                    SetFolderPermissions(seasonFolder);
                 }
             }
 
@@ -136,6 +145,11 @@ namespace NzbDrone.Core.MediaFiles
 
         private void SetPermissions(string path, string permissions)
         {
+            if (!_configService.SetPermissionsLinux)
+            {
+                return;
+            }
+
             try
             {
                 _diskProvider.SetPermissions(path, permissions);
diff --git a/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs b/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs
deleted file mode 100644
index 1b379db76..000000000
--- a/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using NLog;
-using NzbDrone.Common.Disk;
-using NzbDrone.Common.EnvironmentInfo;
-using NzbDrone.Core.Configuration;
-
-namespace NzbDrone.Core.MediaFiles
-{
-    public interface ISetMediaFilePermissions
-    {
-        void SetPermissions(string filename);
-    }
-
-    public class SetMediaFilePermissions : ISetMediaFilePermissions
-    {
-        private readonly IDiskProvider _diskProvider;
-        private readonly IConfigService _configService;
-        private readonly Logger _logger;
-
-        public SetMediaFilePermissions(IDiskProvider diskProvider, IConfigService configService, Logger logger)
-        {
-            _diskProvider = diskProvider;
-            _configService = configService;
-            _logger = logger;
-        }
-
-        public void SetPermissions(string filename)
-        {
-            if (OsInfo.IsWindows)
-            {
-                //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important.
-                try
-                {
-                    _diskProvider.InheritFolderPermissions(filename);
-                }
-                catch (Exception ex)
-                {
-                    if (ex is UnauthorizedAccessException || ex is InvalidOperationException)
-                    {
-                        _logger.Debug("Unable to apply folder permissions to: ", filename);
-                        _logger.TraceException(ex.Message, ex);
-                    }
-
-                    else
-                    {
-                        throw;
-                    }
-                }
-            }
-
-            else
-            {
-                _diskProvider.SetPermissions(filename, _configService.FileChmod);
-            }
-        }
-    }
-}
diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj
index 894868c1c..5dbf4fd48 100644
--- a/src/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/src/NzbDrone.Core/NzbDrone.Core.csproj
@@ -290,7 +290,6 @@
     <Compile Include="MediaFiles\MediaFileExtensions.cs" />
     <Compile Include="MediaFiles\MediaInfo\VideoFileInfoReader.cs" />
     <Compile Include="MediaFiles\RenameEpisodeFilePreview.cs" />
-    <Compile Include="MediaFiles\SetMediaFilePermissions.cs" />
     <Compile Include="Messaging\Commands\CommandExecutor.cs" />
     <Compile Include="Messaging\Commands\ICommandExecutor.cs" />
     <Compile Include="Messaging\Commands\IExecute.cs" />
diff --git a/src/NzbDrone.Mono/DiskProvider.cs b/src/NzbDrone.Mono/DiskProvider.cs
index dedfe57b3..7582eb7c9 100644
--- a/src/NzbDrone.Mono/DiskProvider.cs
+++ b/src/NzbDrone.Mono/DiskProvider.cs
@@ -61,6 +61,13 @@ namespace NzbDrone.Mono
 
                 throw new Exception("Error setting file permissions: " + error);
             }
+
+            if (Syscall.chown(path, Syscall.getuid(), Syscall.getgid()) < 0)
+            {
+                var error = Stdlib.GetLastError();
+
+                throw new Exception("Error setting file owner: " + error);
+            }
         }
 
         public override long? GetTotalSize(string path)
diff --git a/src/UI/Handlebars/Helpers/Os.js b/src/UI/Handlebars/Helpers/Os.js
new file mode 100644
index 000000000..0d46f7391
--- /dev/null
+++ b/src/UI/Handlebars/Helpers/Os.js
@@ -0,0 +1,9 @@
+'use strict';
+define(
+    [
+        'handlebars'
+    ], function (Handlebars) {
+        Handlebars.registerHelper('LinuxOnly', function () {
+            return new Handlebars.SafeString('<i class="icon-linux" title="Linux Only"></i>');
+        });
+    });
diff --git a/src/UI/Handlebars/backbone.marionette.templates.js b/src/UI/Handlebars/backbone.marionette.templates.js
index 2c589a5cb..f849da4f1 100644
--- a/src/UI/Handlebars/backbone.marionette.templates.js
+++ b/src/UI/Handlebars/backbone.marionette.templates.js
@@ -10,6 +10,7 @@ define(
         'Handlebars/Helpers/Series',
         'Handlebars/Helpers/Quality',
         'Handlebars/Helpers/System',
+        'Handlebars/Helpers/Os',
         'Handlebars/Handlebars.Debug'
     ], function (Templates) {
         return function () {
diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayout.js b/src/UI/Settings/MediaManagement/MediaManagementLayout.js
index 56b34c814..3b5ac123e 100644
--- a/src/UI/Settings/MediaManagement/MediaManagementLayout.js
+++ b/src/UI/Settings/MediaManagement/MediaManagementLayout.js
@@ -5,15 +5,17 @@ define(
         'marionette',
         'Settings/MediaManagement/Naming/NamingView',
         'Settings/MediaManagement/Sorting/View',
-        'Settings/MediaManagement/FileManagement/FileManagementView'
-    ], function (Marionette, NamingView, SortingView, FileManagementView) {
+        'Settings/MediaManagement/FileManagement/FileManagementView',
+        'Settings/MediaManagement/Permissions/PermissionsView'
+    ], function (Marionette, NamingView, SortingView, FileManagementView, PermissionsView) {
         return Marionette.Layout.extend({
             template: 'Settings/MediaManagement/MediaManagementLayoutTemplate',
 
             regions: {
                 episodeNaming  : '#episode-naming',
                 sorting        : '#sorting',
-                fileManagement : '#file-management'
+                fileManagement : '#file-management',
+                permissions    : '#permissions'
             },
 
             initialize: function (options) {
@@ -25,6 +27,7 @@ define(
                 this.episodeNaming.show(new NamingView({ model: this.namingSettings }));
                 this.sorting.show(new SortingView({ model: this.settings }));
                 this.fileManagement.show(new FileManagementView({ model: this.settings }));
+                this.permissions.show(new PermissionsView({ model: this.settings }));
             }
         });
     });
diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html
index 4720aa606..05a416998 100644
--- a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html
+++ b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html
@@ -2,4 +2,5 @@
     <div id="episode-naming"></div>
     <div id="sorting"></div>
     <div id="file-management"></div>
+    <div id="permissions"></div>
 </div>
\ No newline at end of file
diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js
new file mode 100644
index 000000000..e1a098106
--- /dev/null
+++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js
@@ -0,0 +1,39 @@
+'use strict';
+define(
+    [
+        'marionette',
+        'Mixins/AsModelBoundView',
+        'Mixins/AutoComplete'
+    ], function (Marionette, AsModelBoundView) {
+
+        var view = Marionette.ItemView.extend({
+            template: 'Settings/MediaManagement/Permissions/PermissionsViewTemplate',
+
+            ui: {
+                recyclingBin                  : '.x-path',
+                failedDownloadHandlingCheckbox: '.x-failed-download-handling',
+                failedDownloadOptions         : '.x-failed-download-options'
+            },
+
+            events: {
+                'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility'
+            },
+
+            onShow: function () {
+                this.ui.recyclingBin.autoComplete('/directories');
+            },
+
+            _setFailedDownloadOptionsVisibility: function () {
+                var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked');
+                if (checked) {
+                    this.ui.failedDownloadOptions.slideDown();
+                }
+
+                else {
+                    this.ui.failedDownloadOptions.slideUp();
+                }
+            }
+        });
+
+        return AsModelBoundView.call(view);
+    });
diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html
new file mode 100644
index 000000000..9528724e6
--- /dev/null
+++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html
@@ -0,0 +1,48 @@
+<fieldset class="advanced-setting">
+    <legend>Permissions</legend>
+
+    <div class="control-group">
+        <label class="control-label">Set Permissions</label>
+
+        <div class="controls">
+            <label class="checkbox toggle well">
+                <input type="checkbox" name="setPermissionsLinux"/>
+                <p>
+                    <span>Yes</span>
+                    <span>No</span>
+                </p>
+
+                <div class="btn btn-primary slide-button"/>
+            </label>
+
+            <span class="help-inline-checkbox">
+                {{LinuxOnly}}
+                <i class="icon-question-sign" title="Should chmod/chown be run when files are imported/renamed?"/>
+            </span>
+        </div>
+    </div>
+
+    <div class="control-group">
+        <label class="control-label">File chmod mask</label>
+
+        <div class="controls">
+            <input type="text" name="fileChmod"/>
+            <span class="help-inline">
+                {{LinuxOnly}}
+                <i class="icon-nd-form-info" title="Octal, applied to media files when imported/renamed by NzbDrone"/>
+            </span>
+        </div>
+    </div>
+
+    <div class="control-group">
+        <label class="control-label">Folder chmod mask</label>
+
+        <div class="controls">
+            <input type="text" name="folderChmod"/>
+            <span class="help-inline">
+                {{LinuxOnly}}
+                <i class="icon-nd-form-info" title="Octal, applied to series/season folders created by NzbDrone"/>
+            </span>
+        </div>
+    </div>
+</fieldset>
\ No newline at end of file