New: Displaying folder-based permissions in UI rather than file-based permissions and with selectable sane presets
Fixed: Preserve setgid when applying unix permissions
This commit is contained in:
parent
850552bf17
commit
d88bb7f855
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
value={value}
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
{
|
||||
isEditable ?
|
||||
<div
|
||||
className={styles.editableContainer}
|
||||
>
|
||||
<TextInput
|
||||
className={className}
|
||||
name={name}
|
||||
value={value}
|
||||
readOnly={isDisabled}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.dropdownArrowContainerEditable,
|
||||
isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer)
|
||||
}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
<div
|
||||
className={isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</Link>
|
||||
</div> :
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
value={value}
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
<div
|
||||
className={isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
</Measure>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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,
|
||||
|
|
|
@ -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 &&
|
||||
<FormInputHelpText
|
||||
text={helpTextWarning}
|
||||
isWarning={true}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
.inputWrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inputFolder {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.inputUnitWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputUnit {
|
||||
composes: inputUnit from '~Components/Form/FormInputGroup.css';
|
||||
|
||||
right: 40px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 5px;
|
||||
margin-left: 17px;
|
||||
line-height: 20px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.unit {
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.readOnly {
|
||||
background-color: #eee;
|
||||
}
|
|
@ -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: <span className={styles.unit}>{v.hint}</span> };
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.inputUnitWrapper}>
|
||||
<EnhancedSelectInput
|
||||
name={name}
|
||||
value={value}
|
||||
values={values}
|
||||
isEditable={true}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={styles.inputUnit}>
|
||||
d{unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<label>UMask</label>
|
||||
<div className={styles.value}>{umask}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Folder</label>
|
||||
<div className={styles.value}>{folder}</div>
|
||||
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>File</label>
|
||||
<div className={styles.value}>{file}</div>
|
||||
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -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
|
||||
];
|
||||
|
|
|
@ -357,7 +357,7 @@ class MediaManagement extends Component {
|
|||
</FieldSet>
|
||||
|
||||
{
|
||||
advancedSettings && isMono &&
|
||||
advancedSettings &&
|
||||
<FieldSet
|
||||
legend="Permissions"
|
||||
>
|
||||
|
@ -382,17 +382,32 @@ class MediaManagement extends Component {
|
|||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>File chmod mode</FormLabel>
|
||||
<FormLabel>chmod Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.UMASK}
|
||||
name="chmodFolder"
|
||||
helpText="Octal, applied during import/rename to media folders and files (without execute bits)"
|
||||
helpTextWarning="This only works if the user running sonarr is the owner of the file. It's better to ensure the download client sets the permissions properly."
|
||||
onChange={onInputChange}
|
||||
{...settings.chmodFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>chown Group</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="fileChmod"
|
||||
helpTexts={[
|
||||
'Octal, applied to media files when imported/renamed by Sonarr',
|
||||
'The same mode is applied to series/season folders with the execute bit added, e.g., 0644 becomes 0755'
|
||||
]}
|
||||
name="chownGroup"
|
||||
helpText="Group name or gid. Use gid for remote file systems."
|
||||
helpTextWarning="This only works if the user running sonarr is the owner of the file. It's better to ensure the download client uses the same group as sonarr."
|
||||
values={fileDateOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.fileChmod}
|
||||
{...settings.chownGroup}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
|
|
@ -8,10 +8,10 @@ namespace NzbDrone.Api.Config
|
|||
{
|
||||
public class MediaManagementConfigModule : NzbDroneConfigModule<MediaManagementConfigResource>
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<FileInfo> GetFileInfos(string path);
|
||||
void RemoveEmptySubfolders(string path);
|
||||
void SaveStream(Stream stream, string path);
|
||||
bool IsValidFilePermissionMask(string mask);
|
||||
bool IsValidFolderPermissionMask(string mask);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ namespace Sonarr.Api.V3.Config
|
|||
{
|
||||
public class MediaManagementConfigModule : SonarrConfigModule<MediaManagementConfigResource>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue