diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css
index 3d2df1ad3..60cd28d69 100644
--- a/frontend/src/Components/Form/EnhancedSelectInput.css
+++ b/frontend/src/Components/Form/EnhancedSelectInput.css
@@ -5,6 +5,10 @@
align-items: center;
}
+.editableContainer {
+ width: 100%;
+}
+
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
@@ -22,6 +26,16 @@
margin-left: 12px;
}
+.dropdownArrowContainerEditable {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding-right: 17px;
+ width: 30%;
+ height: 35px;
+ text-align: right;
+}
+
.dropdownArrowContainerDisabled {
composes: dropdownArrowContainer;
diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js
index 8f89101f0..8705e7310 100644
--- a/frontend/src/Components/Form/EnhancedSelectInput.js
+++ b/frontend/src/Components/Form/EnhancedSelectInput.js
@@ -15,6 +15,7 @@ import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import Scroller from 'Components/Scroller/Scroller';
+import TextInput from './TextInput';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import HintedSelectInputOption from './HintedSelectInputOption';
import styles from './EnhancedSelectInput.css';
@@ -169,11 +170,21 @@ class EnhancedSelectInput extends Component {
}
}
+ onFocus = () => {
+ if (this.state.isOpen) {
+ this._removeListener();
+ this.setState({ isOpen: false });
+ }
+ }
+
onBlur = () => {
- // Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
- const origIndex = getSelectedIndex(this.props);
- if (origIndex !== this.state.selectedIndex) {
- this.setState({ selectedIndex: origIndex });
+ if (!this.props.isEditable) {
+ // Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
+ const origIndex = getSelectedIndex(this.props);
+
+ if (origIndex !== this.state.selectedIndex) {
+ this.setState({ selectedIndex: origIndex });
+ }
}
}
@@ -297,16 +308,19 @@ class EnhancedSelectInput extends Component {
const {
className,
disabledClassName,
+ name,
value,
values,
isDisabled,
+ isEditable,
isFetching,
hasError,
hasWarning,
valueOptions,
selectedValueOptions,
selectedValueComponent: SelectedValueComponent,
- optionComponent: OptionComponent
+ optionComponent: OptionComponent,
+ onChange
} = this.props;
const {
@@ -332,52 +346,94 @@ class EnhancedSelectInput extends Component {
whitelist={['width']}
onMeasure={this.onMeasure}
>
-
-
- {selectedOption ? selectedOption.value : null}
-
+ {
+ isEditable ?
+
+
+
+ {
+ isFetching &&
+
+ }
-
+ {
+ !isFetching &&
+
+ }
+
+
:
+
+
+ {selectedOption ? selectedOption.value : null}
+
- {
- isFetching &&
-
- }
+
- {
- !isFetching &&
-
- }
-
-
+ {
+ isFetching &&
+
+ }
+
+ {
+ !isFetching &&
+
+ }
+
+
+ }
)}
@@ -502,6 +558,7 @@ EnhancedSelectInput.propTypes = {
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
+ isEditable: PropTypes.bool.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired,
@@ -517,6 +574,7 @@ EnhancedSelectInput.defaultProps = {
disabledClassName: styles.isDisabled,
isDisabled: false,
isFetching: false,
+ isEditable: false,
valueOptions: {},
selectedValueOptions: {},
selectedValueComponent: HintedSelectInputSelectedValue,
diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js
index 7437447d1..89e1e81ea 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -23,6 +23,7 @@ import TagInputConnector from './TagInputConnector';
import TagSelectInputConnector from './TagSelectInputConnector';
import TextTagInputConnector from './TextTagInputConnector';
import TextInput from './TextInput';
+import UMaskInput from './UMaskInput';
import FormInputHelpText from './FormInputHelpText';
import styles from './FormInputGroup.css';
@@ -88,6 +89,9 @@ function getComponent(type) {
case inputTypes.TAG_SELECT:
return TagSelectInputConnector;
+ case inputTypes.UMASK:
+ return UMaskInput;
+
default:
return TextInput;
}
@@ -195,7 +199,7 @@ function FormInputGroup(props) {
}
{
- !checkInput && helpTextWarning &&
+ (!checkInput || helpText) && helpTextWarning &&
div {
+ display: flex;
+
+ label {
+ flex: 0 0 50px;
+ }
+
+ .value {
+ width: 50px;
+ text-align: right;
+ }
+
+ .unit {
+ width: 90px;
+ text-align: right;
+ }
+ }
+}
+
+.readOnly {
+ background-color: #eee;
+}
diff --git a/frontend/src/Components/Form/UMaskInput.js b/frontend/src/Components/Form/UMaskInput.js
new file mode 100644
index 000000000..d15d3befb
--- /dev/null
+++ b/frontend/src/Components/Form/UMaskInput.js
@@ -0,0 +1,133 @@
+/* eslint-disable no-bitwise */
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import styles from './UMaskInput.css';
+import EnhancedSelectInput from './EnhancedSelectInput';
+
+const umaskOptions = [
+ {
+ key: '755',
+ value: '755 - Owner write, Everyone else read',
+ hint: 'drwxr-xr-x'
+ },
+ {
+ key: '775',
+ value: '775 - Owner & Group write, Other read',
+ hint: 'drwxrwxr-x'
+ },
+ {
+ key: '770',
+ value: '770 - Owner & Group write',
+ hint: 'drwxrwx---'
+ },
+ {
+ key: '750',
+ value: '750 - Owner write, Group read',
+ hint: 'drwxr-x---'
+ },
+ {
+ key: '777',
+ value: '777 - Everyone write',
+ hint: 'drwxrwxrwx'
+ }
+];
+
+function formatPermissions(permissions) {
+
+ const hasSticky = permissions & 0o1000;
+ const hasSetGID = permissions & 0o2000;
+ const hasSetUID = permissions & 0o4000;
+
+ let result = '';
+
+ for (let i = 0; i < 9; i++) {
+ const bit = (permissions & (1 << i)) !== 0;
+ let digit = bit ? 'xwr'[i % 3] : '-';
+ if (i === 6 && hasSetUID) {
+ digit = bit ? 's' : 'S';
+ } else if (i === 3 && hasSetGID) {
+ digit = bit ? 's' : 'S';
+ } else if (i === 0 && hasSticky) {
+ digit = bit ? 't' : 'T';
+ }
+ result = digit + result;
+ }
+
+ return result;
+}
+
+class UMaskInput extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const valueNum = parseInt(value, 8);
+ const umaskNum = 0o777 & ~valueNum;
+ const umask = umaskNum.toString(8).padStart(4, '0');
+ const folderNum = 0o777 & ~umaskNum;
+ const folder = folderNum.toString(8).padStart(3, '0');
+ const fileNum = 0o666 & ~umaskNum;
+ const file = fileNum.toString(8).padStart(3, '0');
+
+ const unit = formatPermissions(folderNum);
+
+ const values = umaskOptions.map((v) => {
+ return { ...v, hint: {v.hint} };
+ });
+
+ return (
+
+
+
+
+
+
+
{folder}
+
d{formatPermissions(folderNum)}
+
+
+
+
{file}
+
{formatPermissions(fileNum)}
+
+
+
+ );
+ }
+}
+
+UMaskInput.propTypes = {
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ hasError: PropTypes.bool,
+ hasWarning: PropTypes.bool,
+ onChange: PropTypes.func.isRequired,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func
+};
+
+export default UMaskInput;
diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js
index d1d5de185..b38bc9096 100644
--- a/frontend/src/Helpers/Props/inputTypes.js
+++ b/frontend/src/Helpers/Props/inputTypes.js
@@ -19,6 +19,7 @@ export const TAG = 'tag';
export const TEXT = 'text';
export const TEXT_TAG = 'textTag';
export const TAG_SELECT = 'tagSelect';
+export const UMASK = 'umask';
export const all = [
AUTO_COMPLETE,
@@ -41,5 +42,6 @@ export const all = [
TAG,
TEXT,
TEXT_TAG,
- TAG_SELECT
+ TAG_SELECT,
+ UMASK
];
diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js
index 9dbba5fb9..44c4b4460 100644
--- a/frontend/src/Settings/MediaManagement/MediaManagement.js
+++ b/frontend/src/Settings/MediaManagement/MediaManagement.js
@@ -357,7 +357,7 @@ class MediaManagement extends Component {
{
- advancedSettings && isMono &&
+ advancedSettings &&
diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs b/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs
index 17894db46..76c240dcf 100644
--- a/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs
+++ b/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs
@@ -8,10 +8,10 @@ namespace NzbDrone.Api.Config
{
public class MediaManagementConfigModule : NzbDroneConfigModule
{
- public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FileChmodValidator fileChmodValidator)
+ public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
: base(configService)
{
- SharedValidator.RuleFor(c => c.FileChmod).SetValidator(fileChmodValidator).When(c => !string.IsNullOrEmpty(c.FileChmod) && PlatformInfo.IsMono);
+ SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && PlatformInfo.IsMono);
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
}
diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs
index 740f81454..feb13eb34 100644
--- a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs
+++ b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs
@@ -15,7 +15,8 @@ namespace NzbDrone.Api.Config
public FileDateType FileDate { get; set; }
public bool SetPermissionsLinux { get; set; }
- public string FileChmod { get; set; }
+ public string ChmodFolder { get; set; }
+ public string ChownGroup { get; set; }
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public bool CopyUsingHardlinks { get; set; }
@@ -38,7 +39,8 @@ namespace NzbDrone.Api.Config
FileDate = model.FileDate,
SetPermissionsLinux = model.SetPermissionsLinux,
- FileChmod = model.FileChmod,
+ ChmodFolder = model.ChmodFolder,
+ ChownGroup = model.ChownGroup,
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
CopyUsingHardlinks = model.CopyUsingHardlinks,
diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs
index dfdccfd28..79176480f 100644
--- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs
+++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs
@@ -32,7 +32,7 @@ namespace NzbDrone.Common.Disk
public abstract long? GetAvailableSpace(string path);
public abstract void InheritFolderPermissions(string filename);
public abstract void SetEveryonePermissions(string filename);
- public abstract void SetPermissions(string path, string mask);
+ public abstract void SetPermissions(string path, string mask, string group);
public abstract void CopyPermissions(string sourcePath, string targetPath);
public abstract long? GetTotalSize(string path);
@@ -509,7 +509,7 @@ namespace NzbDrone.Common.Disk
}
}
- public virtual bool IsValidFilePermissionMask(string mask)
+ public virtual bool IsValidFolderPermissionMask(string mask)
{
throw new NotSupportedException();
}
diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs
index 672e7a207..efe0d70f6 100644
--- a/src/NzbDrone.Common/Disk/IDiskProvider.cs
+++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs
@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Disk
long? GetAvailableSpace(string path);
void InheritFolderPermissions(string filename);
void SetEveryonePermissions(string filename);
- void SetPermissions(string path, string mask);
+ void SetPermissions(string path, string mask, string group);
void CopyPermissions(string sourcePath, string targetPath);
long? GetTotalSize(string path);
DateTime FolderGetCreationTime(string path);
@@ -55,6 +55,6 @@ namespace NzbDrone.Common.Disk
List GetFileInfos(string path);
void RemoveEmptySubfolders(string path);
void SaveStream(Stream stream, string path);
- bool IsValidFilePermissionMask(string mask);
+ bool IsValidFolderPermissionMask(string mask);
}
}
diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs
index 9a74663f0..d2844ccbe 100644
--- a/src/NzbDrone.Core/Configuration/ConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/ConfigService.cs
@@ -252,11 +252,18 @@ namespace NzbDrone.Core.Configuration
set { SetValue("SetPermissionsLinux", value); }
}
- public string FileChmod
+ public string ChmodFolder
{
- get { return GetValue("FileChmod", "0644"); }
+ get { return GetValue("ChmodFolder", "755"); }
- set { SetValue("FileChmod", value); }
+ set { SetValue("ChmodFolder", value); }
+ }
+
+ public string ChownGroup
+ {
+ get { return GetValue("ChownGroup", ""); }
+
+ set { SetValue("ChownGroup", value); }
}
public int FirstDayOfWeek
diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs
index bdad8d54e..ad3bef22c 100644
--- a/src/NzbDrone.Core/Configuration/IConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/IConfigService.cs
@@ -43,7 +43,8 @@ namespace NzbDrone.Core.Configuration
//Permissions (Media Management)
bool SetPermissionsLinux { get; set; }
- string FileChmod { get; set; }
+ string ChmodFolder { get; set; }
+ string ChownGroup { get; set; }
//Indexers
int Retention { get; set; }
diff --git a/src/NzbDrone.Core/Datastore/Migration/147_swap_filechmod_for_folderchmod.cs b/src/NzbDrone.Core/Datastore/Migration/147_swap_filechmod_for_folderchmod.cs
new file mode 100644
index 000000000..3e0c53652
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/147_swap_filechmod_for_folderchmod.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Data;
+using FluentMigrator;
+using NzbDrone.Common.Disk;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+ [Migration(147)]
+ public class swap_filechmod_for_folderchmod : NzbDroneMigrationBase
+ {
+ protected override void MainDbUpgrade()
+ {
+ // Reverts part of migration 140, note that the v1 of migration140 also removed chowngroup
+ Execute.WithConnection(ConvertFileChmodToFolderChmod);
+ }
+ private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tran)
+ {
+ using (IDbCommand getFileChmodCmd = conn.CreateCommand())
+ {
+ getFileChmodCmd.Transaction = tran;
+ getFileChmodCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'filechmod'";
+
+ var fileChmod = getFileChmodCmd.ExecuteScalar() as string;
+ if (fileChmod != null)
+ {
+ if (fileChmod.IsNotNullOrWhiteSpace())
+ {
+ // Convert without using mono libraries. We take the 'r' bits and shifting them to the 'x' position, preserving everything else.
+ var fileChmodNum = Convert.ToInt32(fileChmod, 8);
+ var folderChmodNum = fileChmodNum | ((fileChmodNum & 0x124) >> 2);
+ var folderChmod = Convert.ToString(folderChmodNum, 8).PadLeft(3, '0');
+
+ using (IDbCommand insertCmd = conn.CreateCommand())
+ {
+ insertCmd.Transaction = tran;
+ insertCmd.CommandText = "INSERT INTO Config (Key, Value) VALUES ('chmodfolder', ?)";
+ insertCmd.AddParameter(folderChmod);
+
+ insertCmd.ExecuteNonQuery();
+ }
+ }
+
+ using (IDbCommand deleteCmd = conn.CreateCommand())
+ {
+ deleteCmd.Transaction = tran;
+ deleteCmd.CommandText = "DELETE FROM Config WHERE Key = 'filechmod'";
+
+ deleteCmd.ExecuteNonQuery();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs
index 0b96799ca..0c68b6baa 100644
--- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs
+++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs
@@ -194,13 +194,10 @@ namespace NzbDrone.Core.MediaFiles
try
{
- var permissions = _configService.FileChmod;
- _diskProvider.SetPermissions(path, permissions);
+ _diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup);
}
-
catch (Exception ex)
{
-
_logger.Warn(ex, "Unable to apply permissions to: " + path);
_logger.Debug(ex, ex.Message);
}
diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileAttributeService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileAttributeService.cs
index 52a8b37aa..03e3c6bda 100644
--- a/src/NzbDrone.Core/MediaFiles/MediaFileAttributeService.cs
+++ b/src/NzbDrone.Core/MediaFiles/MediaFileAttributeService.cs
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.MediaFiles
else
{
- SetMonoPermissions(path, _configService.FileChmod);
+ SetMonoPermissions(path);
}
}
@@ -63,7 +63,7 @@ namespace NzbDrone.Core.MediaFiles
{
if (OsInfo.IsNotWindows)
{
- SetMonoPermissions(path, _configService.FileChmod);
+ SetMonoPermissions(path);
}
}
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.MediaFiles
}
}
- private void SetMonoPermissions(string path, string permissions)
+ private void SetMonoPermissions(string path)
{
if (!_configService.SetPermissionsLinux)
{
@@ -85,7 +85,7 @@ namespace NzbDrone.Core.MediaFiles
try
{
- _diskProvider.SetPermissions(path, permissions);
+ _diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup);
}
catch (Exception ex)
diff --git a/src/NzbDrone.Core/Validation/FileChmodValidator.cs b/src/NzbDrone.Core/Validation/FileChmodValidator.cs
index c1a8dba44..d0150a868 100644
--- a/src/NzbDrone.Core/Validation/FileChmodValidator.cs
+++ b/src/NzbDrone.Core/Validation/FileChmodValidator.cs
@@ -3,11 +3,11 @@ using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Validation
{
- public class FileChmodValidator : PropertyValidator
+ public class FolderChmodValidator : PropertyValidator
{
private readonly IDiskProvider _diskProvider;
- public FileChmodValidator(IDiskProvider diskProvider)
+ public FolderChmodValidator(IDiskProvider diskProvider)
: base("Must contain a valid Unix permissions octal")
{
_diskProvider = diskProvider;
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Validation
{
if (context.PropertyValue == null) return false;
- return _diskProvider.IsValidFilePermissionMask(context.PropertyValue.ToString());
+ return _diskProvider.IsValidFolderPermissionMask(context.PropertyValue.ToString());
}
}
}
\ No newline at end of file
diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs
index 445224826..500eab149 100644
--- a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs
+++ b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs
@@ -170,15 +170,15 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
Syscall.stat(tempFile, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0444");
- Subject.SetPermissions(tempFile, "644");
+ Subject.SetPermissions(tempFile, "755", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
- Subject.SetPermissions(tempFile, "0644");
+ Subject.SetPermissions(tempFile, "0755", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
- Subject.SetPermissions(tempFile, "1664");
+ Subject.SetPermissions(tempFile, "1775", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1664");
}
@@ -195,51 +195,49 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
Syscall.stat(tempPath, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0555");
- Subject.SetPermissions(tempPath, "644");
+ Subject.SetPermissions(tempPath, "755", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
- Subject.SetPermissions(tempPath, "0644");
+ Subject.SetPermissions(tempPath, "0755", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
- Subject.SetPermissions(tempPath, "1664");
+ Subject.SetPermissions(tempPath, "1775", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1775");
- Subject.SetPermissions(tempPath, "775");
+ Subject.SetPermissions(tempPath, "775", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
- Subject.SetPermissions(tempPath, "640");
+ Subject.SetPermissions(tempPath, "750", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0750");
- Subject.SetPermissions(tempPath, "0041");
+ Subject.SetPermissions(tempPath, "0051", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0051");
}
[Test]
- public void IsValidFilePermissionMask_should_return_correct()
+ public void IsValidFolderPermissionMask_should_return_correct()
{
- // Files may not be executable
- Subject.IsValidFilePermissionMask("0777").Should().BeFalse();
- Subject.IsValidFilePermissionMask("0544").Should().BeFalse();
- Subject.IsValidFilePermissionMask("0454").Should().BeFalse();
- Subject.IsValidFilePermissionMask("0445").Should().BeFalse();
-
// No special bits should be set
- Subject.IsValidFilePermissionMask("1644").Should().BeFalse();
- Subject.IsValidFilePermissionMask("2644").Should().BeFalse();
- Subject.IsValidFilePermissionMask("4644").Should().BeFalse();
- Subject.IsValidFilePermissionMask("7644").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("1755").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("2755").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("4755").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("7755").Should().BeFalse();
- // Files should be readable and writeable by owner
- Subject.IsValidFilePermissionMask("0400").Should().BeFalse();
- Subject.IsValidFilePermissionMask("0000").Should().BeFalse();
- Subject.IsValidFilePermissionMask("0200").Should().BeFalse();
- Subject.IsValidFilePermissionMask("0600").Should().BeTrue();
+ // Folder should be readable and writeable by owner
+ Subject.IsValidFolderPermissionMask("0000").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("0100").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("0200").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("0300").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("0400").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("0500").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("0600").Should().BeFalse();
+ Subject.IsValidFolderPermissionMask("0700").Should().BeTrue();
}
}
}
diff --git a/src/NzbDrone.Mono/Disk/DiskProvider.cs b/src/NzbDrone.Mono/Disk/DiskProvider.cs
index 41da17e3a..628df8d59 100644
--- a/src/NzbDrone.Mono/Disk/DiskProvider.cs
+++ b/src/NzbDrone.Mono/Disk/DiskProvider.cs
@@ -66,50 +66,66 @@ namespace NzbDrone.Mono.Disk
}
- public override void SetPermissions(string path, string mask)
+ public override void SetPermissions(string path, string mask, string group)
{
Logger.Debug("Setting permissions: {0} on {1}", mask, path);
var permissions = NativeConvert.FromOctalPermissionString(mask);
- if (Directory.Exists(path))
+ if (File.Exists(path))
{
- permissions = GetFolderPermissions(permissions);
+ permissions = GetFilePermissions(permissions);
}
+ // Preserve non-access permissions
+ if (Syscall.stat(path, out var curStat) < 0)
+ {
+ var error = Stdlib.GetLastError();
+
+ throw new LinuxPermissionsException("Error getting current permissions: " + error);
+ }
+
+ permissions |= curStat.st_mode & ~FilePermissions.ACCESSPERMS;
+
if (Syscall.chmod(path, permissions) < 0)
{
var error = Stdlib.GetLastError();
throw new LinuxPermissionsException("Error setting permissions: " + error);
}
+
+ var groupId = GetGroupId(group);
+
+ if (Syscall.chown(path, unchecked((uint)-1), groupId) < 0)
+ {
+ var error = Stdlib.GetLastError();
+
+ throw new LinuxPermissionsException("Error setting group: " + error);
+ }
}
- private static FilePermissions GetFolderPermissions(FilePermissions permissions)
+ private static FilePermissions GetFilePermissions(FilePermissions permissions)
{
- permissions |= (FilePermissions) ((int) (permissions & (FilePermissions.S_IRUSR | FilePermissions.S_IRGRP | FilePermissions.S_IROTH)) >> 2);
+ permissions &= ~(FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH);
return permissions;
}
- public override bool IsValidFilePermissionMask(string mask)
+ public override bool IsValidFolderPermissionMask(string mask)
{
try
{
var permissions = NativeConvert.FromOctalPermissionString(mask);
- if ((permissions & (FilePermissions.S_ISUID | FilePermissions.S_ISGID | FilePermissions.S_ISVTX)) != 0)
+ if ((permissions & ~FilePermissions.ACCESSPERMS) != 0)
{
+ // Only allow access permissions
return false;
}
- if ((permissions & (FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH)) != 0)
- {
- return false;
- }
-
- if ((permissions & (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) != (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR))
+ if ((permissions & FilePermissions.S_IRWXU) != FilePermissions.S_IRWXU)
{
+ // We expect at least full owner permissions (700)
return false;
}
diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs
index 5eae3d815..eb96d4084 100644
--- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs
+++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs
@@ -135,7 +135,7 @@ namespace NzbDrone.Update.UpdateEngine
{
// Old MacOS App stores Sonarr binaries in MacOS together with shell script
// Make shim executable
- _diskProvider.SetPermissions(shimPath, "0755");
+ _diskProvider.SetPermissions(shimPath, "755", null);
}
}
}
diff --git a/src/NzbDrone.Windows/Disk/DiskProvider.cs b/src/NzbDrone.Windows/Disk/DiskProvider.cs
index 8a7a45a52..05c58262f 100644
--- a/src/NzbDrone.Windows/Disk/DiskProvider.cs
+++ b/src/NzbDrone.Windows/Disk/DiskProvider.cs
@@ -88,7 +88,7 @@ namespace NzbDrone.Windows.Disk
}
- public override void SetPermissions(string path, string mask)
+ public override void SetPermissions(string path, string mask, string group)
{
}
diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs
index 5a09620a3..566ea93eb 100644
--- a/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs
+++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs
@@ -8,11 +8,11 @@ namespace Sonarr.Api.V3.Config
{
public class MediaManagementConfigModule : SonarrConfigModule
{
- public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FileChmodValidator fileChmodValidator)
+ public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
: base(configService)
{
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
- SharedValidator.RuleFor(c => c.FileChmod).SetValidator(fileChmodValidator).When(c => !string.IsNullOrEmpty(c.FileChmod) && PlatformInfo.IsMono);
+ SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && PlatformInfo.IsMono);
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
}
diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs
index 4039530e0..16b4586a1 100644
--- a/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs
+++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs
@@ -18,7 +18,8 @@ namespace Sonarr.Api.V3.Config
public RescanAfterRefreshType RescanAfterRefresh { get; set; }
public bool SetPermissionsLinux { get; set; }
- public string FileChmod { get; set; }
+ public string ChmodFolder { get; set; }
+ public string ChownGroup { get; set; }
public EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
@@ -45,7 +46,8 @@ namespace Sonarr.Api.V3.Config
RescanAfterRefresh = model.RescanAfterRefresh,
SetPermissionsLinux = model.SetPermissionsLinux,
- FileChmod = model.FileChmod,
+ ChmodFolder = model.ChmodFolder,
+ ChownGroup = model.ChownGroup,
EpisodeTitleRequired = model.EpisodeTitleRequired,
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,