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;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editableContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
composes: hasError from '~Components/Form/Input.css';
|
composes: hasError from '~Components/Form/Input.css';
|
||||||
}
|
}
|
||||||
|
@ -22,6 +26,16 @@
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdownArrowContainerEditable {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-right: 17px;
|
||||||
|
width: 30%;
|
||||||
|
height: 35px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdownArrowContainerDisabled {
|
.dropdownArrowContainerDisabled {
|
||||||
composes: dropdownArrowContainer;
|
composes: dropdownArrowContainer;
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Measure from 'Components/Measure';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import TextInput from './TextInput';
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||||
import styles from './EnhancedSelectInput.css';
|
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 = () => {
|
onBlur = () => {
|
||||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
if (!this.props.isEditable) {
|
||||||
const origIndex = getSelectedIndex(this.props);
|
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||||
if (origIndex !== this.state.selectedIndex) {
|
const origIndex = getSelectedIndex(this.props);
|
||||||
this.setState({ selectedIndex: origIndex });
|
|
||||||
|
if (origIndex !== this.state.selectedIndex) {
|
||||||
|
this.setState({ selectedIndex: origIndex });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,16 +308,19 @@ class EnhancedSelectInput extends Component {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
disabledClassName,
|
disabledClassName,
|
||||||
|
name,
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
isEditable,
|
||||||
isFetching,
|
isFetching,
|
||||||
hasError,
|
hasError,
|
||||||
hasWarning,
|
hasWarning,
|
||||||
valueOptions,
|
valueOptions,
|
||||||
selectedValueOptions,
|
selectedValueOptions,
|
||||||
selectedValueComponent: SelectedValueComponent,
|
selectedValueComponent: SelectedValueComponent,
|
||||||
optionComponent: OptionComponent
|
optionComponent: OptionComponent,
|
||||||
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -332,52 +346,94 @@ class EnhancedSelectInput extends Component {
|
||||||
whitelist={['width']}
|
whitelist={['width']}
|
||||||
onMeasure={this.onMeasure}
|
onMeasure={this.onMeasure}
|
||||||
>
|
>
|
||||||
<Link
|
{
|
||||||
className={classNames(
|
isEditable ?
|
||||||
className,
|
<div
|
||||||
hasError && styles.hasError,
|
className={styles.editableContainer}
|
||||||
hasWarning && styles.hasWarning,
|
>
|
||||||
isDisabled && disabledClassName
|
<TextInput
|
||||||
)}
|
className={className}
|
||||||
isDisabled={isDisabled}
|
name={name}
|
||||||
onBlur={this.onBlur}
|
value={value}
|
||||||
onKeyDown={this.onKeyDown}
|
readOnly={isDisabled}
|
||||||
onPress={this.onPress}
|
hasError={hasError}
|
||||||
>
|
hasWarning={hasWarning}
|
||||||
<SelectedValueComponent
|
onFocus={this.onFocus}
|
||||||
value={value}
|
onBlur={this.onBlur}
|
||||||
values={values}
|
onChange={onChange}
|
||||||
{...selectedValueOptions}
|
/>
|
||||||
{...selectedOption}
|
<Link
|
||||||
isDisabled={isDisabled}
|
className={classNames(
|
||||||
isMultiSelect={isMultiSelect}
|
styles.dropdownArrowContainerEditable,
|
||||||
>
|
isDisabled ?
|
||||||
{selectedOption ? selectedOption.value : null}
|
styles.dropdownArrowContainerDisabled :
|
||||||
</SelectedValueComponent>
|
styles.dropdownArrowContainer)
|
||||||
|
}
|
||||||
|
onPress={this.onPress}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
{
|
||||||
className={isDisabled ?
|
!isFetching &&
|
||||||
styles.dropdownArrowContainerDisabled :
|
<Icon
|
||||||
styles.dropdownArrowContainer
|
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>
|
||||||
|
|
||||||
{
|
<div
|
||||||
isFetching &&
|
className={isDisabled ?
|
||||||
<LoadingIndicator
|
styles.dropdownArrowContainerDisabled :
|
||||||
className={styles.loading}
|
styles.dropdownArrowContainer
|
||||||
size={20}
|
}
|
||||||
/>
|
>
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching &&
|
isFetching &&
|
||||||
<Icon
|
<LoadingIndicator
|
||||||
name={icons.CARET_DOWN}
|
className={styles.loading}
|
||||||
/>
|
size={20}
|
||||||
}
|
/>
|
||||||
</div>
|
}
|
||||||
</Link>
|
|
||||||
|
{
|
||||||
|
!isFetching &&
|
||||||
|
<Icon
|
||||||
|
name={icons.CARET_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</Measure>
|
</Measure>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -502,6 +558,7 @@ EnhancedSelectInput.propTypes = {
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
hasWarning: PropTypes.bool,
|
hasWarning: PropTypes.bool,
|
||||||
valueOptions: PropTypes.object.isRequired,
|
valueOptions: PropTypes.object.isRequired,
|
||||||
|
@ -517,6 +574,7 @@ EnhancedSelectInput.defaultProps = {
|
||||||
disabledClassName: styles.isDisabled,
|
disabledClassName: styles.isDisabled,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
isEditable: false,
|
||||||
valueOptions: {},
|
valueOptions: {},
|
||||||
selectedValueOptions: {},
|
selectedValueOptions: {},
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import TagInputConnector from './TagInputConnector';
|
||||||
import TagSelectInputConnector from './TagSelectInputConnector';
|
import TagSelectInputConnector from './TagSelectInputConnector';
|
||||||
import TextTagInputConnector from './TextTagInputConnector';
|
import TextTagInputConnector from './TextTagInputConnector';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
import UMaskInput from './UMaskInput';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import styles from './FormInputGroup.css';
|
import styles from './FormInputGroup.css';
|
||||||
|
|
||||||
|
@ -88,6 +89,9 @@ function getComponent(type) {
|
||||||
case inputTypes.TAG_SELECT:
|
case inputTypes.TAG_SELECT:
|
||||||
return TagSelectInputConnector;
|
return TagSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.UMASK:
|
||||||
|
return UMaskInput;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return TextInput;
|
return TextInput;
|
||||||
}
|
}
|
||||||
|
@ -195,7 +199,7 @@ function FormInputGroup(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!checkInput && helpTextWarning &&
|
(!checkInput || helpText) && helpTextWarning &&
|
||||||
<FormInputHelpText
|
<FormInputHelpText
|
||||||
text={helpTextWarning}
|
text={helpTextWarning}
|
||||||
isWarning={true}
|
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 = 'text';
|
||||||
export const TEXT_TAG = 'textTag';
|
export const TEXT_TAG = 'textTag';
|
||||||
export const TAG_SELECT = 'tagSelect';
|
export const TAG_SELECT = 'tagSelect';
|
||||||
|
export const UMASK = 'umask';
|
||||||
|
|
||||||
export const all = [
|
export const all = [
|
||||||
AUTO_COMPLETE,
|
AUTO_COMPLETE,
|
||||||
|
@ -41,5 +42,6 @@ export const all = [
|
||||||
TAG,
|
TAG,
|
||||||
TEXT,
|
TEXT,
|
||||||
TEXT_TAG,
|
TEXT_TAG,
|
||||||
TAG_SELECT
|
TAG_SELECT,
|
||||||
|
UMASK
|
||||||
];
|
];
|
||||||
|
|
|
@ -357,7 +357,7 @@ class MediaManagement extends Component {
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
{
|
{
|
||||||
advancedSettings && isMono &&
|
advancedSettings &&
|
||||||
<FieldSet
|
<FieldSet
|
||||||
legend="Permissions"
|
legend="Permissions"
|
||||||
>
|
>
|
||||||
|
@ -382,17 +382,32 @@ class MediaManagement extends Component {
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
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
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="fileChmod"
|
name="chownGroup"
|
||||||
helpTexts={[
|
helpText="Group name or gid. Use gid for remote file systems."
|
||||||
'Octal, applied to media files when imported/renamed by Sonarr',
|
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."
|
||||||
'The same mode is applied to series/season folders with the execute bit added, e.g., 0644 becomes 0755'
|
values={fileDateOptions}
|
||||||
]}
|
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.fileChmod}
|
{...settings.chownGroup}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
|
@ -8,10 +8,10 @@ namespace NzbDrone.Api.Config
|
||||||
{
|
{
|
||||||
public class MediaManagementConfigModule : NzbDroneConfigModule<MediaManagementConfigResource>
|
public class MediaManagementConfigModule : NzbDroneConfigModule<MediaManagementConfigResource>
|
||||||
{
|
{
|
||||||
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FileChmodValidator fileChmodValidator)
|
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
|
||||||
: base(configService)
|
: 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));
|
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 FileDateType FileDate { get; set; }
|
||||||
|
|
||||||
public bool SetPermissionsLinux { 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 SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||||
public bool CopyUsingHardlinks { get; set; }
|
public bool CopyUsingHardlinks { get; set; }
|
||||||
|
@ -38,7 +39,8 @@ namespace NzbDrone.Api.Config
|
||||||
FileDate = model.FileDate,
|
FileDate = model.FileDate,
|
||||||
|
|
||||||
SetPermissionsLinux = model.SetPermissionsLinux,
|
SetPermissionsLinux = model.SetPermissionsLinux,
|
||||||
FileChmod = model.FileChmod,
|
ChmodFolder = model.ChmodFolder,
|
||||||
|
ChownGroup = model.ChownGroup,
|
||||||
|
|
||||||
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
||||||
CopyUsingHardlinks = model.CopyUsingHardlinks,
|
CopyUsingHardlinks = model.CopyUsingHardlinks,
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace NzbDrone.Common.Disk
|
||||||
public abstract long? GetAvailableSpace(string path);
|
public abstract long? GetAvailableSpace(string path);
|
||||||
public abstract void InheritFolderPermissions(string filename);
|
public abstract void InheritFolderPermissions(string filename);
|
||||||
public abstract void SetEveryonePermissions(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 void CopyPermissions(string sourcePath, string targetPath);
|
||||||
public abstract long? GetTotalSize(string path);
|
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();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace NzbDrone.Common.Disk
|
||||||
long? GetAvailableSpace(string path);
|
long? GetAvailableSpace(string path);
|
||||||
void InheritFolderPermissions(string filename);
|
void InheritFolderPermissions(string filename);
|
||||||
void SetEveryonePermissions(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);
|
void CopyPermissions(string sourcePath, string targetPath);
|
||||||
long? GetTotalSize(string path);
|
long? GetTotalSize(string path);
|
||||||
DateTime FolderGetCreationTime(string path);
|
DateTime FolderGetCreationTime(string path);
|
||||||
|
@ -55,6 +55,6 @@ namespace NzbDrone.Common.Disk
|
||||||
List<FileInfo> GetFileInfos(string path);
|
List<FileInfo> GetFileInfos(string path);
|
||||||
void RemoveEmptySubfolders(string path);
|
void RemoveEmptySubfolders(string path);
|
||||||
void SaveStream(Stream stream, 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); }
|
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
|
public int FirstDayOfWeek
|
||||||
|
|
|
@ -43,7 +43,8 @@ namespace NzbDrone.Core.Configuration
|
||||||
|
|
||||||
//Permissions (Media Management)
|
//Permissions (Media Management)
|
||||||
bool SetPermissionsLinux { get; set; }
|
bool SetPermissionsLinux { get; set; }
|
||||||
string FileChmod { get; set; }
|
string ChmodFolder { get; set; }
|
||||||
|
string ChownGroup { get; set; }
|
||||||
|
|
||||||
//Indexers
|
//Indexers
|
||||||
int Retention { get; set; }
|
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
|
try
|
||||||
{
|
{
|
||||||
var permissions = _configService.FileChmod;
|
_diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup);
|
||||||
_diskProvider.SetPermissions(path, permissions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
_logger.Warn(ex, "Unable to apply permissions to: " + path);
|
_logger.Warn(ex, "Unable to apply permissions to: " + path);
|
||||||
_logger.Debug(ex, ex.Message);
|
_logger.Debug(ex, ex.Message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SetMonoPermissions(path, _configService.FileChmod);
|
SetMonoPermissions(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
{
|
{
|
||||||
if (OsInfo.IsNotWindows)
|
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)
|
if (!_configService.SetPermissionsLinux)
|
||||||
{
|
{
|
||||||
|
@ -85,7 +85,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_diskProvider.SetPermissions(path, permissions);
|
_diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
@ -3,11 +3,11 @@ using NzbDrone.Common.Disk;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Validation
|
namespace NzbDrone.Core.Validation
|
||||||
{
|
{
|
||||||
public class FileChmodValidator : PropertyValidator
|
public class FolderChmodValidator : PropertyValidator
|
||||||
{
|
{
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
|
||||||
public FileChmodValidator(IDiskProvider diskProvider)
|
public FolderChmodValidator(IDiskProvider diskProvider)
|
||||||
: base("Must contain a valid Unix permissions octal")
|
: base("Must contain a valid Unix permissions octal")
|
||||||
{
|
{
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
|
@ -17,7 +17,7 @@ namespace NzbDrone.Core.Validation
|
||||||
{
|
{
|
||||||
if (context.PropertyValue == null) return false;
|
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);
|
Syscall.stat(tempFile, out var fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0444");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0444");
|
||||||
|
|
||||||
Subject.SetPermissions(tempFile, "644");
|
Subject.SetPermissions(tempFile, "755", null);
|
||||||
Syscall.stat(tempFile, out fileStat);
|
Syscall.stat(tempFile, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
|
||||||
|
|
||||||
Subject.SetPermissions(tempFile, "0644");
|
Subject.SetPermissions(tempFile, "0755", null);
|
||||||
Syscall.stat(tempFile, out fileStat);
|
Syscall.stat(tempFile, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
|
||||||
|
|
||||||
Subject.SetPermissions(tempFile, "1664");
|
Subject.SetPermissions(tempFile, "1775", null);
|
||||||
Syscall.stat(tempFile, out fileStat);
|
Syscall.stat(tempFile, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1664");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1664");
|
||||||
}
|
}
|
||||||
|
@ -195,51 +195,49 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
|
||||||
Syscall.stat(tempPath, out var fileStat);
|
Syscall.stat(tempPath, out var fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0555");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0555");
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "644");
|
Subject.SetPermissions(tempPath, "755", null);
|
||||||
Syscall.stat(tempPath, out fileStat);
|
Syscall.stat(tempPath, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "0644");
|
Subject.SetPermissions(tempPath, "0755", null);
|
||||||
Syscall.stat(tempPath, out fileStat);
|
Syscall.stat(tempPath, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "1664");
|
Subject.SetPermissions(tempPath, "1775", null);
|
||||||
Syscall.stat(tempPath, out fileStat);
|
Syscall.stat(tempPath, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1775");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1775");
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "775");
|
Subject.SetPermissions(tempPath, "775", null);
|
||||||
Syscall.stat(tempPath, out fileStat);
|
Syscall.stat(tempPath, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "640");
|
Subject.SetPermissions(tempPath, "750", null);
|
||||||
Syscall.stat(tempPath, out fileStat);
|
Syscall.stat(tempPath, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0750");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0750");
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "0041");
|
Subject.SetPermissions(tempPath, "0051", null);
|
||||||
Syscall.stat(tempPath, out fileStat);
|
Syscall.stat(tempPath, out fileStat);
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0051");
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0051");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[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
|
// No special bits should be set
|
||||||
Subject.IsValidFilePermissionMask("1644").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("1755").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("2644").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("2755").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("4644").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("4755").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("7644").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("7755").Should().BeFalse();
|
||||||
|
|
||||||
// Files should be readable and writeable by owner
|
// Folder should be readable and writeable by owner
|
||||||
Subject.IsValidFilePermissionMask("0400").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("0000").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("0000").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("0100").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("0200").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("0200").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("0600").Should().BeTrue();
|
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);
|
Logger.Debug("Setting permissions: {0} on {1}", mask, path);
|
||||||
|
|
||||||
var permissions = NativeConvert.FromOctalPermissionString(mask);
|
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)
|
if (Syscall.chmod(path, permissions) < 0)
|
||||||
{
|
{
|
||||||
var error = Stdlib.GetLastError();
|
var error = Stdlib.GetLastError();
|
||||||
|
|
||||||
throw new LinuxPermissionsException("Error setting permissions: " + error);
|
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;
|
return permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsValidFilePermissionMask(string mask)
|
public override bool IsValidFolderPermissionMask(string mask)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var permissions = NativeConvert.FromOctalPermissionString(mask);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((permissions & (FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH)) != 0)
|
if ((permissions & FilePermissions.S_IRWXU) != FilePermissions.S_IRWXU)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((permissions & (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) != (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR))
|
|
||||||
{
|
{
|
||||||
|
// We expect at least full owner permissions (700)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ namespace NzbDrone.Update.UpdateEngine
|
||||||
{
|
{
|
||||||
// Old MacOS App stores Sonarr binaries in MacOS together with shell script
|
// Old MacOS App stores Sonarr binaries in MacOS together with shell script
|
||||||
// Make shim executable
|
// 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 class MediaManagementConfigModule : SonarrConfigModule<MediaManagementConfigResource>
|
||||||
{
|
{
|
||||||
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FileChmodValidator fileChmodValidator)
|
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
|
||||||
: base(configService)
|
: base(configService)
|
||||||
{
|
{
|
||||||
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
|
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.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
|
||||||
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
|
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ namespace Sonarr.Api.V3.Config
|
||||||
public RescanAfterRefreshType RescanAfterRefresh { get; set; }
|
public RescanAfterRefreshType RescanAfterRefresh { get; set; }
|
||||||
|
|
||||||
public bool SetPermissionsLinux { 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 EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
|
||||||
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
|
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||||
|
@ -45,7 +46,8 @@ namespace Sonarr.Api.V3.Config
|
||||||
RescanAfterRefresh = model.RescanAfterRefresh,
|
RescanAfterRefresh = model.RescanAfterRefresh,
|
||||||
|
|
||||||
SetPermissionsLinux = model.SetPermissionsLinux,
|
SetPermissionsLinux = model.SetPermissionsLinux,
|
||||||
FileChmod = model.FileChmod,
|
ChmodFolder = model.ChmodFolder,
|
||||||
|
ChownGroup = model.ChownGroup,
|
||||||
|
|
||||||
EpisodeTitleRequired = model.EpisodeTitleRequired,
|
EpisodeTitleRequired = model.EpisodeTitleRequired,
|
||||||
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
||||||
|
|
Loading…
Reference in New Issue