mirror of
https://github.com/Radarr/Radarr.git
synced 2024-08-18 08:19:38 +02:00
New: Dynamic Select and UMask Fields
Fixes #5380 Fixes #5348 Fixes #5167 Fixes #5166 Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
This commit is contained in:
parent
73ce77f1ca
commit
9c77399379
@ -2,34 +2,22 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { clearOptions, fetchOptions } from 'Store/Actions/providerOptionActions';
|
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||||
import DeviceInput from './DeviceInput';
|
import DeviceInput from './DeviceInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(state, { name }) => name,
|
(state) => state.providerOptions.devices || defaultState,
|
||||||
(state) => state.providerOptions,
|
(value, devices) => {
|
||||||
(value, name, devices) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = devices;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isFetching,
|
...devices,
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items: items[name] || [],
|
|
||||||
selectedDevices: value.map((valueDevice) => {
|
selectedDevices: value.map((valueDevice) => {
|
||||||
const sectionItems = items[name] || [];
|
|
||||||
|
|
||||||
// Disable equality ESLint rule so we don't need to worry about
|
// Disable equality ESLint rule so we don't need to worry about
|
||||||
// a type mismatch between the value items and the device ID.
|
// a type mismatch between the value items and the device ID.
|
||||||
// eslint-disable-next-line eqeqeq
|
// eslint-disable-next-line eqeqeq
|
||||||
const device = sectionItems.find((d) => d.id == valueDevice);
|
const device = devices.items.find((d) => d.id == valueDevice);
|
||||||
|
|
||||||
if (device) {
|
if (device) {
|
||||||
return {
|
return {
|
||||||
@ -63,7 +51,7 @@ class DeviceInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
this.props.dispatchClearOptions();
|
this.props.dispatchClearOptions({ section: 'devices' });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -73,14 +61,12 @@ class DeviceInputConnector extends Component {
|
|||||||
const {
|
const {
|
||||||
provider,
|
provider,
|
||||||
providerData,
|
providerData,
|
||||||
dispatchFetchOptions,
|
dispatchFetchOptions
|
||||||
requestAction,
|
|
||||||
name
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatchFetchOptions({
|
dispatchFetchOptions({
|
||||||
action: requestAction,
|
section: 'devices',
|
||||||
itemSection: name,
|
action: 'getDevices',
|
||||||
provider,
|
provider,
|
||||||
providerData
|
providerData
|
||||||
});
|
});
|
||||||
@ -109,7 +95,6 @@ class DeviceInputConnector extends Component {
|
|||||||
DeviceInputConnector.propTypes = {
|
DeviceInputConnector.propTypes = {
|
||||||
provider: PropTypes.string.isRequired,
|
provider: PropTypes.string.isRequired,
|
||||||
providerData: PropTypes.object.isRequired,
|
providerData: PropTypes.object.isRequired,
|
||||||
requestAction: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -66,3 +80,8 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px -5px 5px 0;
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import React, { Component } from 'react';
|
|||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Measure from 'Components/Measure';
|
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';
|
||||||
@ -16,6 +17,7 @@ import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
@ -168,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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +262,10 @@ class EnhancedSelectInput extends Component {
|
|||||||
this._addListener();
|
this._addListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.isOpen && this.props.onOpen) {
|
||||||
|
this.props.onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,15 +308,19 @@ class EnhancedSelectInput extends Component {
|
|||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
disabledClassName,
|
disabledClassName,
|
||||||
|
name,
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
isEditable,
|
||||||
|
isFetching,
|
||||||
hasError,
|
hasError,
|
||||||
hasWarning,
|
hasWarning,
|
||||||
valueOptions,
|
valueOptions,
|
||||||
selectedValueOptions,
|
selectedValueOptions,
|
||||||
selectedValueComponent: SelectedValueComponent,
|
selectedValueComponent: SelectedValueComponent,
|
||||||
optionComponent: OptionComponent
|
optionComponent: OptionComponent,
|
||||||
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -326,40 +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}
|
||||||
}
|
/>
|
||||||
>
|
}
|
||||||
<Icon
|
</Link>
|
||||||
name={icons.CARET_DOWN}
|
</div> :
|
||||||
/>
|
<Link
|
||||||
</div>
|
className={classNames(
|
||||||
</Link>
|
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
|
||||||
|
className={isDisabled ?
|
||||||
|
styles.dropdownArrowContainerDisabled :
|
||||||
|
styles.dropdownArrowContainer
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching &&
|
||||||
|
<Icon
|
||||||
|
name={icons.CARET_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</Measure>
|
</Measure>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -483,12 +557,15 @@ EnhancedSelectInput.propTypes = {
|
|||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: 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,
|
||||||
selectedValueOptions: PropTypes.object.isRequired,
|
selectedValueOptions: PropTypes.object.isRequired,
|
||||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||||
optionComponent: PropTypes.elementType,
|
optionComponent: PropTypes.elementType,
|
||||||
|
onOpen: PropTypes.func,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -496,6 +573,8 @@ EnhancedSelectInput.defaultProps = {
|
|||||||
className: styles.enhancedSelect,
|
className: styles.enhancedSelect,
|
||||||
disabledClassName: styles.isDisabled,
|
disabledClassName: styles.isDisabled,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
isFetching: false,
|
||||||
|
isEditable: false,
|
||||||
valueOptions: {},
|
valueOptions: {},
|
||||||
selectedValueOptions: {},
|
selectedValueOptions: {},
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||||
|
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
const importantFieldNames = [
|
||||||
|
'baseUrl',
|
||||||
|
'apiPath',
|
||||||
|
'apiKey'
|
||||||
|
];
|
||||||
|
|
||||||
|
function getProviderDataKey(providerData) {
|
||||||
|
if (!providerData || !providerData.fields) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = providerData.fields
|
||||||
|
.filter((f) => importantFieldNames.includes(f.name))
|
||||||
|
.map((f) => f.value);
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectOptions(items) {
|
||||||
|
if (!items) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((option) => {
|
||||||
|
return {
|
||||||
|
key: option.value,
|
||||||
|
value: option.name,
|
||||||
|
hint: option.hint,
|
||||||
|
parentKey: option.parentValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
|
||||||
|
(options) => {
|
||||||
|
if (options) {
|
||||||
|
return {
|
||||||
|
isFetching: options.isFetching,
|
||||||
|
values: getSelectOptions(options.items)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchOptions: fetchOptions,
|
||||||
|
dispatchClearOptions: clearOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
class EnhancedSelectInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
refetchRequired: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate = (prevProps) => {
|
||||||
|
const prevKey = getProviderDataKey(prevProps.providerData);
|
||||||
|
const nextKey = getProviderDataKey(this.props.providerData);
|
||||||
|
|
||||||
|
if (!_.isEqual(prevKey, nextKey)) {
|
||||||
|
this.setState({ refetchRequired: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
this._cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOpen = () => {
|
||||||
|
if (this.state.refetchRequired) {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
_populate() {
|
||||||
|
const {
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
selectOptionsProviderAction,
|
||||||
|
dispatchFetchOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
this.setState({ refetchRequired: false });
|
||||||
|
dispatchFetchOptions({
|
||||||
|
section: selectOptionsProviderAction,
|
||||||
|
action: selectOptionsProviderAction,
|
||||||
|
provider,
|
||||||
|
providerData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_cleanup() {
|
||||||
|
const {
|
||||||
|
selectOptionsProviderAction,
|
||||||
|
dispatchClearOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
dispatchClearOptions({ section: selectOptionsProviderAction });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...this.props}
|
||||||
|
onOpen={this.onOpen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EnhancedSelectInputConnector.propTypes = {
|
||||||
|
provider: PropTypes.string.isRequired,
|
||||||
|
providerData: PropTypes.object.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectOptionsProviderAction: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||||
|
dispatchClearOptions: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);
|
@ -32,6 +32,7 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
|
depth,
|
||||||
isSelected,
|
isSelected,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isHidden,
|
isHidden,
|
||||||
@ -54,6 +55,11 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
depth !== 0 &&
|
||||||
|
<div style={{ width: `${depth * 20}px` }} />
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isMultiSelect &&
|
isMultiSelect &&
|
||||||
<CheckInput
|
<CheckInput
|
||||||
@ -84,6 +90,7 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
EnhancedSelectInputOption.propTypes = {
|
EnhancedSelectInputOption.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
depth: PropTypes.number.isRequired,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isHidden: PropTypes.bool.isRequired,
|
isHidden: PropTypes.bool.isRequired,
|
||||||
@ -95,6 +102,7 @@ EnhancedSelectInputOption.propTypes = {
|
|||||||
|
|
||||||
EnhancedSelectInputOption.defaultProps = {
|
EnhancedSelectInputOption.defaultProps = {
|
||||||
className: styles.option,
|
className: styles.option,
|
||||||
|
depth: 0,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
isMultiSelect: false
|
isMultiSelect: false
|
||||||
|
@ -9,6 +9,7 @@ import CaptchaInputConnector from './CaptchaInputConnector';
|
|||||||
import CheckInput from './CheckInput';
|
import CheckInput from './CheckInput';
|
||||||
import DeviceInputConnector from './DeviceInputConnector';
|
import DeviceInputConnector from './DeviceInputConnector';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
@ -24,6 +25,7 @@ import TagSelectInputConnector from './TagSelectInputConnector';
|
|||||||
import TextArea from './TextArea';
|
import TextArea from './TextArea';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import TextTagInputConnector from './TextTagInputConnector';
|
import TextTagInputConnector from './TextTagInputConnector';
|
||||||
|
import UMaskInput from './UMaskInput';
|
||||||
import styles from './FormInputGroup.css';
|
import styles from './FormInputGroup.css';
|
||||||
|
|
||||||
function getComponent(type) {
|
function getComponent(type) {
|
||||||
@ -73,6 +75,8 @@ function getComponent(type) {
|
|||||||
case inputTypes.SELECT:
|
case inputTypes.SELECT:
|
||||||
return EnhancedSelectInput;
|
return EnhancedSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
|
return EnhancedSelectInputConnector;
|
||||||
case inputTypes.TAG:
|
case inputTypes.TAG:
|
||||||
return TagInputConnector;
|
return TagInputConnector;
|
||||||
|
|
||||||
@ -85,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;
|
||||||
}
|
}
|
||||||
@ -192,7 +199,7 @@ function FormInputGroup(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!checkInput && helpTextWarning &&
|
(!checkInput || helpText) && helpTextWarning &&
|
||||||
<FormInputHelpText
|
<FormInputHelpText
|
||||||
text={helpTextWarning}
|
text={helpTextWarning}
|
||||||
isWarning={true}
|
isWarning={true}
|
||||||
|
@ -9,6 +9,7 @@ function HintedSelectInputOption(props) {
|
|||||||
id,
|
id,
|
||||||
value,
|
value,
|
||||||
hint,
|
hint,
|
||||||
|
depth,
|
||||||
isSelected,
|
isSelected,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isMultiSelect,
|
isMultiSelect,
|
||||||
@ -19,6 +20,7 @@ function HintedSelectInputOption(props) {
|
|||||||
return (
|
return (
|
||||||
<EnhancedSelectInputOption
|
<EnhancedSelectInputOption
|
||||||
id={id}
|
id={id}
|
||||||
|
depth={depth}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isHidden={isDisabled}
|
isHidden={isDisabled}
|
||||||
@ -48,6 +50,7 @@ HintedSelectInputOption.propTypes = {
|
|||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
hint: PropTypes.node,
|
hint: PropTypes.node,
|
||||||
|
depth: PropTypes.number,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
isMultiSelect: PropTypes.bool.isRequired,
|
||||||
|
@ -98,7 +98,9 @@ class KeyValueListInput extends Component {
|
|||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
keyPlaceholder,
|
keyPlaceholder,
|
||||||
valuePlaceholder
|
valuePlaceholder,
|
||||||
|
hasError,
|
||||||
|
hasWarning
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { isFocused } = this.state;
|
const { isFocused } = this.state;
|
||||||
@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
className,
|
className,
|
||||||
isFocused && styles.isFocused
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
|||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
|
||||||
function getType(type, value) {
|
function getType({ type, selectOptionsProviderAction }) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'captcha':
|
case 'captcha':
|
||||||
return inputTypes.CAPTCHA;
|
return inputTypes.CAPTCHA;
|
||||||
@ -23,6 +23,9 @@ function getType(type, value) {
|
|||||||
case 'filePath':
|
case 'filePath':
|
||||||
return inputTypes.PATH;
|
return inputTypes.PATH;
|
||||||
case 'select':
|
case 'select':
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
return inputTypes.DYNAMIC_SELECT;
|
||||||
|
}
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
return inputTypes.TEXT_TAG;
|
return inputTypes.TEXT_TAG;
|
||||||
@ -63,7 +66,6 @@ function ProviderFieldFormGroup(props) {
|
|||||||
value,
|
value,
|
||||||
type,
|
type,
|
||||||
advanced,
|
advanced,
|
||||||
requestAction,
|
|
||||||
hidden,
|
hidden,
|
||||||
pending,
|
pending,
|
||||||
errors,
|
errors,
|
||||||
@ -88,7 +90,7 @@ function ProviderFieldFormGroup(props) {
|
|||||||
<FormLabel>{label}</FormLabel>
|
<FormLabel>{label}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={getType(type, value)}
|
type={getType(props)}
|
||||||
name={name}
|
name={name}
|
||||||
label={label}
|
label={label}
|
||||||
helpText={helpText}
|
helpText={helpText}
|
||||||
@ -100,7 +102,6 @@ function ProviderFieldFormGroup(props) {
|
|||||||
pending={pending}
|
pending={pending}
|
||||||
includeFiles={type === 'filePath' ? true : undefined}
|
includeFiles={type === 'filePath' ? true : undefined}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
requestAction={requestAction}
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@ -109,7 +110,8 @@ function ProviderFieldFormGroup(props) {
|
|||||||
|
|
||||||
const selectOptionsShape = {
|
const selectOptionsShape = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.number.isRequired
|
value: PropTypes.number.isRequired,
|
||||||
|
hint: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
ProviderFieldFormGroup.propTypes = {
|
ProviderFieldFormGroup.propTypes = {
|
||||||
@ -121,12 +123,12 @@ ProviderFieldFormGroup.propTypes = {
|
|||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
advanced: PropTypes.bool.isRequired,
|
advanced: PropTypes.bool.isRequired,
|
||||||
requestAction: PropTypes.string,
|
|
||||||
hidden: PropTypes.string,
|
hidden: PropTypes.string,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
||||||
|
selectOptionsProviderAction: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
53
frontend/src/Components/Form/UMaskInput.css
Normal file
53
frontend/src/Components/Form/UMaskInput.css
Normal file
@ -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;
|
||||||
|
}
|
133
frontend/src/Components/Form/UMaskInput.js
Normal file
133
frontend/src/Components/Form/UMaskInput.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
import styles from './UMaskInput.css';
|
||||||
|
|
||||||
|
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;
|
@ -12,11 +12,13 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
|||||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||||
export const SELECT = 'select';
|
export const SELECT = 'select';
|
||||||
|
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
export const TEXT = 'text';
|
export const TEXT = 'text';
|
||||||
export const TEXT_AREA = 'textArea';
|
export const TEXT_AREA = 'textArea';
|
||||||
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,
|
||||||
@ -33,9 +35,11 @@ export const all = [
|
|||||||
ROOT_FOLDER_SELECT,
|
ROOT_FOLDER_SELECT,
|
||||||
INDEXER_FLAGS_SELECT,
|
INDEXER_FLAGS_SELECT,
|
||||||
SELECT,
|
SELECT,
|
||||||
|
DYNAMIC_SELECT,
|
||||||
TAG,
|
TAG,
|
||||||
TEXT,
|
TEXT,
|
||||||
TEXT_AREA,
|
TEXT_AREA,
|
||||||
TEXT_TAG,
|
TEXT_TAG,
|
||||||
TAG_SELECT
|
TAG_SELECT,
|
||||||
|
UMASK
|
||||||
];
|
];
|
||||||
|
@ -364,17 +364,32 @@ class MediaManagement extends Component {
|
|||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
>
|
>
|
||||||
<FormLabel>{translate('FileChmodMode')}</FormLabel>
|
<FormLabel>{translate('ChmodFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.UMASK}
|
||||||
|
name="chmodFolder"
|
||||||
|
helpText={translate('ChmodFolderHelpText')}
|
||||||
|
helpTextWarning={translate('ChmodFolderHelpTextWarning')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...settings.chmodFolder}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('ChmodGroup')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="fileChmod"
|
name="chownGroup"
|
||||||
helpTexts={[
|
helpText={translate('ChmodGroupHelpText')}
|
||||||
translate('FileChmodHelpTexts1'),
|
helpTextWarning={translate('ChmodGroupHelpTextWarning')}
|
||||||
translate('FileChmodHelpTexts2')
|
values={fileDateOptions}
|
||||||
]}
|
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.fileChmod}
|
{...settings.chownGroup}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import requestAction from 'Utilities/requestAction';
|
import requestAction from 'Utilities/requestAction';
|
||||||
@ -10,11 +11,14 @@ import createHandleActions from './Creators/createHandleActions';
|
|||||||
|
|
||||||
export const section = 'providerOptions';
|
export const section = 'providerOptions';
|
||||||
|
|
||||||
|
const lastActions = {};
|
||||||
|
let lastActionId = 0;
|
||||||
|
|
||||||
//
|
//
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
items: {},
|
items: [],
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: false
|
error: false
|
||||||
@ -23,8 +27,8 @@ export const defaultState = {
|
|||||||
//
|
//
|
||||||
// Actions Types
|
// Actions Types
|
||||||
|
|
||||||
export const FETCH_OPTIONS = 'devices/fetchOptions';
|
export const FETCH_OPTIONS = 'providers/fetchOptions';
|
||||||
export const CLEAR_OPTIONS = 'devices/clearOptions';
|
export const CLEAR_OPTIONS = 'providers/clearOptions';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
@ -38,35 +42,55 @@ export const clearOptions = createAction(CLEAR_OPTIONS);
|
|||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
[FETCH_OPTIONS]: function(getState, payload, dispatch) {
|
[FETCH_OPTIONS]: function(getState, payload, dispatch) {
|
||||||
|
const subsection = `${section}.${payload.section}`;
|
||||||
|
|
||||||
|
if (lastActions[payload.section] && _.isEqual(payload, lastActions[payload.section].payload)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionId = ++lastActionId;
|
||||||
|
|
||||||
|
lastActions[payload.section] = {
|
||||||
|
actionId,
|
||||||
|
payload
|
||||||
|
};
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section: subsection,
|
||||||
isFetching: true
|
isFetching: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const oldItems = getState().providerOptions.items;
|
|
||||||
const itemSection = payload.itemSection;
|
|
||||||
|
|
||||||
const promise = requestAction(payload);
|
const promise = requestAction(payload);
|
||||||
|
|
||||||
promise.done((data) => {
|
promise.done((data) => {
|
||||||
oldItems[itemSection] = data.options || [];
|
if (lastActions[payload.section]) {
|
||||||
|
if (lastActions[payload.section].actionId === actionId) {
|
||||||
|
lastActions[payload.section] = null;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section: subsection,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: true,
|
isPopulated: true,
|
||||||
error: null,
|
error: null,
|
||||||
items: oldItems
|
items: data.options || []
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
promise.fail((xhr) => {
|
||||||
dispatch(set({
|
if (lastActions[payload.section]) {
|
||||||
section,
|
if (lastActions[payload.section].actionId === actionId) {
|
||||||
isFetching: false,
|
lastActions[payload.section] = null;
|
||||||
isPopulated: false,
|
}
|
||||||
error: xhr
|
|
||||||
}));
|
dispatch(set({
|
||||||
|
section: subsection,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -76,8 +100,12 @@ export const actionHandlers = handleThunks({
|
|||||||
|
|
||||||
export const reducers = createHandleActions({
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
[CLEAR_OPTIONS]: function(state) {
|
[CLEAR_OPTIONS]: function(state, { payload }) {
|
||||||
return updateSectionState(state, section, defaultState);
|
const subsection = `${section}.${payload.section}`;
|
||||||
|
|
||||||
|
lastActions[payload.section] = null;
|
||||||
|
|
||||||
|
return updateSectionState(state, subsection, defaultState);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, defaultState, section);
|
}, {}, section);
|
||||||
|
@ -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) && (OsInfo.IsLinux || OsInfo.IsOsx));
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ public class MediaManagementConfigResource : RestResource
|
|||||||
public bool PathsDefaultStatic { get; set; }
|
public bool PathsDefaultStatic { 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; }
|
||||||
@ -39,7 +40,8 @@ public static MediaManagementConfigResource ToResource(IConfigService model)
|
|||||||
AutoRenameFolders = model.AutoRenameFolders,
|
AutoRenameFolders = model.AutoRenameFolders,
|
||||||
|
|
||||||
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,
|
||||||
|
@ -30,7 +30,7 @@ public static StringComparison PathStringComparison
|
|||||||
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);
|
||||||
|
|
||||||
@ -539,7 +539,7 @@ public void SaveStream(Stream stream, string path)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool IsValidFilePermissionMask(string mask)
|
public virtual bool IsValidFolderPermissionMask(string mask)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ public interface IDiskProvider
|
|||||||
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);
|
||||||
@ -56,6 +56,6 @@ public interface IDiskProvider
|
|||||||
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,10 @@ public FieldDefinitionAttribute(int order)
|
|||||||
public FieldType Type { get; set; }
|
public FieldType Type { get; set; }
|
||||||
public bool Advanced { get; set; }
|
public bool Advanced { get; set; }
|
||||||
public Type SelectOptions { get; set; }
|
public Type SelectOptions { get; set; }
|
||||||
|
public string SelectOptionsProviderAction { get; set; }
|
||||||
public string Section { get; set; }
|
public string Section { get; set; }
|
||||||
public HiddenType Hidden { get; set; }
|
public HiddenType Hidden { get; set; }
|
||||||
public PrivacyLevel Privacy { get; set; }
|
public PrivacyLevel Privacy { get; set; }
|
||||||
public string RequestAction { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
||||||
@ -39,6 +39,15 @@ public FieldOptionAttribute(string label = null, [CallerLineNumber] int order =
|
|||||||
public string Hint { get; set; }
|
public string Hint { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FieldSelectOption
|
||||||
|
{
|
||||||
|
public int Value { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Order { get; set; }
|
||||||
|
public string Hint { get; set; }
|
||||||
|
public int? ParentValue { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public enum FieldType
|
public enum FieldType
|
||||||
{
|
{
|
||||||
Textbox,
|
Textbox,
|
||||||
|
@ -317,11 +317,18 @@ public bool SetPermissionsLinux
|
|||||||
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
|
||||||
|
@ -45,7 +45,8 @@ public interface IConfigService
|
|||||||
|
|
||||||
//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; }
|
||||||
|
@ -8,7 +8,7 @@ public class remove_chown_and_folderchmod_config : NzbDroneMigrationBase
|
|||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser', 'chowngroup', 'parsingleniency')");
|
Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser', 'parsingleniency')");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(187)]
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -91,8 +91,8 @@ public override object RequestAction(string action, IDictionary<string, string>
|
|||||||
options = devices.OrderBy(d => d.Name, StringComparer.InvariantCultureIgnoreCase)
|
options = devices.OrderBy(d => d.Name, StringComparer.InvariantCultureIgnoreCase)
|
||||||
.Select(d => new
|
.Select(d => new
|
||||||
{
|
{
|
||||||
id = d.Id,
|
Value = d.Id,
|
||||||
name = d.Name
|
Name = d.Name
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -106,8 +106,8 @@ public override object RequestAction(string action, IDictionary<string, string>
|
|||||||
options = devices.OrderBy(d => d.Label, StringComparer.InvariantCultureIgnoreCase)
|
options = devices.OrderBy(d => d.Label, StringComparer.InvariantCultureIgnoreCase)
|
||||||
.Select(d => new
|
.Select(d => new
|
||||||
{
|
{
|
||||||
id = d.Id,
|
Value = d.Id,
|
||||||
name = d.Label
|
Name = d.Label
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -34,10 +34,10 @@ public RadarrSettings()
|
|||||||
[FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "Apikey of the Radarr V3 instance to import from")]
|
[FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "Apikey of the Radarr V3 instance to import from")]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Type = FieldType.Device, RequestAction = "getProfiles", Label = "Profiles", HelpText = "Profiles from the source instance to import from")]
|
[FieldDefinition(2, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "Profiles", HelpText = "Profiles from the source instance to import from")]
|
||||||
public IEnumerable<int> ProfileIds { get; set; }
|
public IEnumerable<int> ProfileIds { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(3, Type = FieldType.Device, RequestAction = "getTags", Label = "Tags", HelpText = "Tags from the source instance to import from")]
|
[FieldDefinition(3, Type = FieldType.Select, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "Tags from the source instance to import from")]
|
||||||
public IEnumerable<int> TagIds { get; set; }
|
public IEnumerable<int> TagIds { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
|
@ -321,7 +321,14 @@ protected virtual ValidationFailure TestConnection()
|
|||||||
_indexerStatusService.UpdateCookies(Definition.Id, cookies, expiration);
|
_indexerStatusService.UpdateCookies(Definition.Id, cookies, expiration);
|
||||||
};
|
};
|
||||||
var generator = GetRequestGenerator();
|
var generator = GetRequestGenerator();
|
||||||
var releases = FetchPage(generator.GetRecentRequests().GetAllTiers().First().First(), parser);
|
var firstRequest = generator.GetRecentRequests().GetAllTiers().FirstOrDefault()?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (firstRequest == null)
|
||||||
|
{
|
||||||
|
return new ValidationFailure(string.Empty, "No rss feed query available. This may be an issue with the indexer or your indexer category settings.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var releases = FetchPage(firstRequest, parser);
|
||||||
|
|
||||||
if (releases.Empty())
|
if (releases.Empty())
|
||||||
{
|
{
|
||||||
|
@ -159,5 +159,31 @@ protected virtual ValidationFailure TestCapabilities()
|
|||||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (action == "newznabCategories")
|
||||||
|
{
|
||||||
|
List<NewznabCategory> categories = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Settings.BaseUrl.IsNotNullOrWhiteSpace() && Settings.ApiPath.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
categories = _capabilitiesProvider.GetCapabilities(Settings).Categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Use default categories
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
options = NewznabCategoryFieldOptionsConverter.GetFieldSelectOptions(categories)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.RequestAction(action, query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ private NewznabCapabilities FetchCapabilities(NewznabSettings indexerSettings)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var request = new HttpRequest(url, HttpAccept.Rss);
|
var request = new HttpRequest(url, HttpAccept.Rss);
|
||||||
|
request.AllowAutoRedirect = true;
|
||||||
|
|
||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ private NewznabCapabilities FetchCapabilities(NewznabSettings indexerSettings)
|
|||||||
{
|
{
|
||||||
ex.WithData(response, 128 * 1024);
|
ex.WithData(response, 128 * 1024);
|
||||||
_logger.Trace("Unexpected Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content);
|
_logger.Trace("Unexpected Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content);
|
||||||
|
_logger.Error(ex, "Failed to determine newznab api capabilities for {0}, using the defaults instead till Radarr restarts", indexerSettings.BaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return capabilities;
|
return capabilities;
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
|
{
|
||||||
|
public static class NewznabCategoryFieldOptionsConverter
|
||||||
|
{
|
||||||
|
public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories)
|
||||||
|
{
|
||||||
|
// Categories not relevant for Radarr
|
||||||
|
var ignoreCategories = new[] { 1000, 3000, 4000, 6000, 7000 };
|
||||||
|
|
||||||
|
// And maybe relevant for specific users
|
||||||
|
var unimportantCategories = new[] { 0, 5000 };
|
||||||
|
|
||||||
|
var result = new List<FieldSelectOption>();
|
||||||
|
|
||||||
|
if (categories == null)
|
||||||
|
{
|
||||||
|
// Fetching categories failed, use default Newznab categories
|
||||||
|
categories = new List<NewznabCategory>();
|
||||||
|
categories.Add(new NewznabCategory
|
||||||
|
{
|
||||||
|
Id = 2000,
|
||||||
|
Name = "Movies",
|
||||||
|
Subcategories = new List<NewznabCategory>
|
||||||
|
{
|
||||||
|
new NewznabCategory { Id = 2010, Name = "Foreign" },
|
||||||
|
new NewznabCategory { Id = 2020, Name = "Other" },
|
||||||
|
new NewznabCategory { Id = 2030, Name = "SD" },
|
||||||
|
new NewznabCategory { Id = 2040, Name = "HD" },
|
||||||
|
new NewznabCategory { Id = 2050, Name = "BluRay" },
|
||||||
|
new NewznabCategory { Id = 2060, Name = "3D" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var category in categories.Where(cat => !ignoreCategories.Contains(cat.Id)).OrderBy(cat => unimportantCategories.Contains(cat.Id)).ThenBy(cat => cat.Id))
|
||||||
|
{
|
||||||
|
result.Add(new FieldSelectOption
|
||||||
|
{
|
||||||
|
Value = category.Id,
|
||||||
|
Name = category.Name,
|
||||||
|
Hint = $"({category.Id})"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (category.Subcategories != null)
|
||||||
|
{
|
||||||
|
foreach (var subcat in category.Subcategories.OrderBy(cat => cat.Id))
|
||||||
|
{
|
||||||
|
result.Add(new FieldSelectOption
|
||||||
|
{
|
||||||
|
Value = subcat.Id,
|
||||||
|
Name = subcat.Name,
|
||||||
|
Hint = $"({subcat.Id})",
|
||||||
|
ParentValue = category.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -75,7 +75,7 @@ public NewznabSettings()
|
|||||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable all categories", Advanced = true)]
|
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Comma Separated list, leave blank to disable all categories", Advanced = true)]
|
||||||
public IEnumerable<int> Categories { get; set; }
|
public IEnumerable<int> Categories { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||||
|
@ -148,5 +148,28 @@ protected virtual ValidationFailure TestCapabilities()
|
|||||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (action == "newznabCategories")
|
||||||
|
{
|
||||||
|
List<NewznabCategory> categories = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
categories = _capabilitiesProvider.GetCapabilities(Settings).Categories;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Use default categories
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
options = NewznabCategoryFieldOptionsConverter.GetFieldSelectOptions(categories)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.RequestAction(action, query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,12 @@
|
|||||||
"ChangeHasNotBeenSavedYet": "Change has not been saved yet",
|
"ChangeHasNotBeenSavedYet": "Change has not been saved yet",
|
||||||
"CheckDownloadClientForDetails": "check download client for more details",
|
"CheckDownloadClientForDetails": "check download client for more details",
|
||||||
"CheckForFinishedDownloadsInterval": "Check For Finished Downloads Interval",
|
"CheckForFinishedDownloadsInterval": "Check For Finished Downloads Interval",
|
||||||
|
"ChmodFolder": "chmod Folder",
|
||||||
|
"ChmodFolderHelpText": "Octal, applied during import/rename to media folders and files (without execute bits)",
|
||||||
|
"ChmodFolderHelpTextWarning": "This only works if the user running Radarr is the owner of the file. It's better to ensure the download client sets the permissions properly.",
|
||||||
|
"ChmodGroup": "chmod Group",
|
||||||
|
"ChmodGroupHelpText": "Group name or gid. Use gid for remote file systems.",
|
||||||
|
"ChmodGroupHelpTextWarning": "This only works if the user running Radarr is the owner of the file. It's better to ensure the download client uses the same group as Radarr.",
|
||||||
"ChooseAnotherFolder": "Choose another Folder",
|
"ChooseAnotherFolder": "Choose another Folder",
|
||||||
"CleanLibraryLevel": "Clean Library Level",
|
"CleanLibraryLevel": "Clean Library Level",
|
||||||
"Clear": "Clear",
|
"Clear": "Clear",
|
||||||
@ -248,9 +254,6 @@
|
|||||||
"Failed": "Failed",
|
"Failed": "Failed",
|
||||||
"FailedDownloadHandling": "Failed Download Handling",
|
"FailedDownloadHandling": "Failed Download Handling",
|
||||||
"FailedLoadingSearchResults": "Failed to load search results, please try again.",
|
"FailedLoadingSearchResults": "Failed to load search results, please try again.",
|
||||||
"FileChmodHelpTexts1": "Octal, applied to media files when imported/renamed by Radarr",
|
|
||||||
"FileChmodHelpTexts2": "The same mode is applied to movie/sub folders with the execute bit added, e.g., 0644 becomes 0755",
|
|
||||||
"FileChmodMode": "File chmod mode",
|
|
||||||
"FileDateHelpText": "Change file date on import/rescan",
|
"FileDateHelpText": "Change file date on import/rescan",
|
||||||
"FileManagement": "File Management",
|
"FileManagement": "File Management",
|
||||||
"Filename": "Filename",
|
"Filename": "Filename",
|
||||||
|
@ -205,8 +205,7 @@ private void SetPermissions(string path)
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var permissions = _configService.FileChmod;
|
_diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup);
|
||||||
_diskProvider.SetPermissions(path, permissions);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -54,7 +54,7 @@ public void SetFilePermissions(string path)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SetMonoPermissions(path, _configService.FileChmod);
|
SetMonoPermissions(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ public void SetFolderPermissions(string path)
|
|||||||
{
|
{
|
||||||
if (OsInfo.IsNotWindows)
|
if (OsInfo.IsNotWindows)
|
||||||
{
|
{
|
||||||
SetMonoPermissions(path, _configService.FileChmod);
|
SetMonoPermissions(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ public void SetFolderLastWriteTime(string path, DateTime time)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetMonoPermissions(string path, string permissions)
|
private void SetMonoPermissions(string path)
|
||||||
{
|
{
|
||||||
if (!_configService.SetPermissionsLinux)
|
if (!_configService.SetPermissionsLinux)
|
||||||
{
|
{
|
||||||
@ -84,7 +84,7 @@ private void SetMonoPermissions(string path, string permissions)
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_diskProvider.SetPermissions(path, permissions);
|
_diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -28,7 +28,7 @@ public PushBulletSettings()
|
|||||||
[FieldDefinition(0, Label = "Access Token", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://www.pushbullet.com/#settings/account")]
|
[FieldDefinition(0, Label = "Access Token", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://www.pushbullet.com/#settings/account")]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs (leave blank to send to all devices)", Type = FieldType.Device, RequestAction = "getDevices")]
|
[FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs (leave blank to send to all devices)", Type = FieldType.Device)]
|
||||||
public IEnumerable<string> DeviceIds { get; set; }
|
public IEnumerable<string> DeviceIds { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Channel Tags", HelpText = "List of Channel Tags to send notifications to", Type = FieldType.Tag)]
|
[FieldDefinition(2, Label = "Channel Tags", HelpText = "List of Channel Tags to send notifications to", Type = FieldType.Tag)]
|
||||||
|
@ -149,7 +149,7 @@ private bool InstallUpdate(UpdatePackage updatePackage)
|
|||||||
// Set executable flag on update app
|
// Set executable flag on update app
|
||||||
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
|
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
|
||||||
{
|
{
|
||||||
_diskProvider.SetPermissions(_appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime), "0755");
|
_diskProvider.SetPermissions(_appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime), "755", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime));
|
_logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime));
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
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;
|
||||||
@ -20,7 +20,7 @@ protected override bool IsValid(PropertyValidatorContext context)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _diskProvider.IsValidFilePermissionMask(context.PropertyValue.ToString());
|
return _diskProvider.IsValidFolderPermissionMask(context.PropertyValue.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,10 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using Radarr.Api.V3.Indexers;
|
||||||
|
using Radarr.Http.ClientSchema;
|
||||||
|
|
||||||
namespace NzbDrone.Integration.Test.ApiTests
|
namespace NzbDrone.Integration.Test.ApiTests
|
||||||
{
|
{
|
||||||
@ -18,5 +21,184 @@ public void should_have_built_in_indexer()
|
|||||||
indexers.Should().NotContain(c => string.IsNullOrWhiteSpace(c.Name));
|
indexers.Should().NotContain(c => string.IsNullOrWhiteSpace(c.Name));
|
||||||
indexers.Where(c => c.ConfigContract == typeof(NullConfig).Name).Should().OnlyContain(c => c.EnableRss);
|
indexers.Where(c => c.ConfigContract == typeof(NullConfig).Name).Should().OnlyContain(c => c.EnableRss);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IndexerResource GetNewznabSchemav2(string name = null)
|
||||||
|
{
|
||||||
|
var schema = Indexers.Schema().First(v => v.Implementation == "Newznab");
|
||||||
|
|
||||||
|
schema.Name = name;
|
||||||
|
schema.EnableRss = false;
|
||||||
|
schema.EnableAutomaticSearch = false;
|
||||||
|
schema.EnableInteractiveSearch = false;
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexerResource GetNewznabSchemav3(string name = null)
|
||||||
|
{
|
||||||
|
var schema = Indexers.Schema().First(v => v.Implementation == "Newznab");
|
||||||
|
|
||||||
|
schema.Name = name;
|
||||||
|
schema.EnableRss = false;
|
||||||
|
schema.EnableAutomaticSearch = false;
|
||||||
|
schema.EnableInteractiveSearch = false;
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field GetCategoriesField(IndexerResource resource)
|
||||||
|
{
|
||||||
|
var field = resource.Fields.First(v => v.Name == "categories");
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v2_categories_should_be_array()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav2();
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value.Should().BeOfType<JArray>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v3_categories_should_be_array()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav3();
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value.Should().BeOfType<JArray>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v2_categories_should_accept_null()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav2("Testv2null");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = null;
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v2_categories_should_accept_emptystring()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav2("Testv2emptystring");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = "";
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v2_categories_should_accept_string()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav2("Testv2string");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = "1000,1010";
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().ToObject<int[]>().Should().BeEquivalentTo(new[] { 1000, 1010 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v2_categories_should_accept_array()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav2("Testv2array");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = new object[] { 1000, 1010 };
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().ToObject<int[]>().Should().BeEquivalentTo(new[] { 1000, 1010 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v3_categories_should_accept_null()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav3("Testv3null");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = null;
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v3_categories_should_accept_emptystring()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav3("Testv3emptystring");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = "";
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v3_categories_should_accept_string()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav3("Testv3string");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = "1000,1010";
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().ToObject<int[]>().Should().BeEquivalentTo(new[] { 1000, 1010 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void v3_categories_should_accept_array()
|
||||||
|
{
|
||||||
|
var schema = GetNewznabSchemav3("Testv3array");
|
||||||
|
|
||||||
|
var categoriesField = GetCategoriesField(schema);
|
||||||
|
|
||||||
|
categoriesField.Value = new object[] { 1000, 1010 };
|
||||||
|
|
||||||
|
var result = Indexers.Post(schema);
|
||||||
|
|
||||||
|
var resultArray = GetCategoriesField(result).Value;
|
||||||
|
resultArray.Should().BeOfType<JArray>();
|
||||||
|
resultArray.As<JArray>().ToObject<int[]>().Should().BeEquivalentTo(new[] { 1000, 1010 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Radarr.Api.V3.Indexers;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Radarr.Api.V3.Indexers;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
|
||||||
namespace NzbDrone.Integration.Test.Client
|
namespace NzbDrone.Integration.Test.Client
|
||||||
@ -9,5 +11,11 @@ public IndexerClient(IRestClient restClient, string apiKey)
|
|||||||
: base(restClient, apiKey)
|
: base(restClient, apiKey)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<IndexerResource> Schema()
|
||||||
|
{
|
||||||
|
var request = BuildRequest("/schema");
|
||||||
|
return Get<List<IndexerResource>>(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,32 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
|
|||||||
[Platform(Exclude = "Win")]
|
[Platform(Exclude = "Win")]
|
||||||
public class DiskProviderFixture : DiskProviderFixtureBase<DiskProvider>
|
public class DiskProviderFixture : DiskProviderFixtureBase<DiskProvider>
|
||||||
{
|
{
|
||||||
|
private string _tempPath;
|
||||||
|
|
||||||
public DiskProviderFixture()
|
public DiskProviderFixture()
|
||||||
{
|
{
|
||||||
PosixOnly();
|
PosixOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void MonoDiskProviderFixtureTearDown()
|
||||||
|
{
|
||||||
|
// Give ourselves back write permissions so we can delete it
|
||||||
|
if (_tempPath != null)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_tempPath))
|
||||||
|
{
|
||||||
|
Syscall.chmod(_tempPath, FilePermissions.S_IRWXU);
|
||||||
|
}
|
||||||
|
else if (File.Exists(_tempPath))
|
||||||
|
{
|
||||||
|
Syscall.chmod(_tempPath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tempPath = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void SetWritePermissions(string path, bool writable)
|
protected override void SetWritePermissions(string path, bool writable)
|
||||||
{
|
{
|
||||||
if (Environment.UserName == "root")
|
if (Environment.UserName == "root")
|
||||||
@ -29,16 +50,37 @@ protected override void SetWritePermissions(string path, bool writable)
|
|||||||
Assert.Inconclusive("Need non-root user to test write permissions.");
|
Assert.Inconclusive("Need non-root user to test write permissions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetWritePermissionsInternal(path, writable, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetWritePermissionsInternal(string path, bool writable, bool setgid)
|
||||||
|
{
|
||||||
// Remove Write permissions, we're still owner so we can clean it up, but we'll have to do that explicitly.
|
// Remove Write permissions, we're still owner so we can clean it up, but we'll have to do that explicitly.
|
||||||
var entry = UnixFileSystemInfo.GetFileSystemEntry(path);
|
Stat stat;
|
||||||
|
Syscall.stat(path, out stat);
|
||||||
|
FilePermissions mode = stat.st_mode;
|
||||||
|
|
||||||
if (writable)
|
if (writable)
|
||||||
{
|
{
|
||||||
entry.FileAccessPermissions |= FileAccessPermissions.UserWrite | FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite;
|
mode |= FilePermissions.S_IWUSR | FilePermissions.S_IWGRP | FilePermissions.S_IWOTH;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entry.FileAccessPermissions &= ~(FileAccessPermissions.UserWrite | FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite);
|
mode &= ~(FilePermissions.S_IWUSR | FilePermissions.S_IWGRP | FilePermissions.S_IWOTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setgid)
|
||||||
|
{
|
||||||
|
mode |= FilePermissions.S_ISGID;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mode &= ~FilePermissions.S_ISGID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.st_mode != mode)
|
||||||
|
{
|
||||||
|
Syscall.chmod(path, mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,21 +206,22 @@ public void should_set_file_permissions()
|
|||||||
var tempFile = GetTempFilePath();
|
var tempFile = GetTempFilePath();
|
||||||
|
|
||||||
File.WriteAllText(tempFile, "File1");
|
File.WriteAllText(tempFile, "File1");
|
||||||
SetWritePermissions(tempFile, false);
|
SetWritePermissionsInternal(tempFile, false, false);
|
||||||
|
_tempPath = tempFile;
|
||||||
|
|
||||||
// Verify test setup
|
// Verify test setup
|
||||||
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");
|
||||||
}
|
}
|
||||||
@ -189,62 +232,118 @@ public void should_set_folder_permissions()
|
|||||||
var tempPath = GetTempFilePath();
|
var tempPath = GetTempFilePath();
|
||||||
|
|
||||||
Directory.CreateDirectory(tempPath);
|
Directory.CreateDirectory(tempPath);
|
||||||
SetWritePermissions(tempPath, false);
|
SetWritePermissionsInternal(tempPath, false, false);
|
||||||
|
_tempPath = tempPath;
|
||||||
|
|
||||||
// Verify test setup
|
// Verify test setup
|
||||||
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, "775", null);
|
||||||
Syscall.stat(tempPath, out fileStat);
|
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
|
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "1664");
|
|
||||||
Syscall.stat(tempPath, out fileStat);
|
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1775");
|
|
||||||
|
|
||||||
Subject.SetPermissions(tempPath, "775");
|
|
||||||
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, "051", 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");
|
||||||
|
|
||||||
// reinstate sane permissions so fokder can be cleaned up
|
|
||||||
Subject.SetPermissions(tempPath, "775");
|
|
||||||
Syscall.stat(tempPath, out fileStat);
|
|
||||||
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void IsValidFilePermissionMask_should_return_correct()
|
public void should_preserve_setgid_on_set_folder_permissions()
|
||||||
{
|
{
|
||||||
// Files may not be executable
|
var tempPath = GetTempFilePath();
|
||||||
Subject.IsValidFilePermissionMask("0777").Should().BeFalse();
|
|
||||||
Subject.IsValidFilePermissionMask("0544").Should().BeFalse();
|
|
||||||
Subject.IsValidFilePermissionMask("0454").Should().BeFalse();
|
|
||||||
Subject.IsValidFilePermissionMask("0445").Should().BeFalse();
|
|
||||||
|
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
SetWritePermissionsInternal(tempPath, false, true);
|
||||||
|
_tempPath = tempPath;
|
||||||
|
|
||||||
|
// Verify test setup
|
||||||
|
Syscall.stat(tempPath, out var fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2555");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "755", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2755");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "775", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2775");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "750", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2750");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "051", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2051");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_clear_setgid_on_set_folder_permissions()
|
||||||
|
{
|
||||||
|
var tempPath = GetTempFilePath();
|
||||||
|
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
SetWritePermissionsInternal(tempPath, false, true);
|
||||||
|
_tempPath = tempPath;
|
||||||
|
|
||||||
|
// Verify test setup
|
||||||
|
Syscall.stat(tempPath, out var fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2555");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "0755", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "0775", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "0750", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0750");
|
||||||
|
|
||||||
|
Subject.SetPermissions(tempPath, "0051", null);
|
||||||
|
Syscall.stat(tempPath, out fileStat);
|
||||||
|
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0051");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsValidFolderPermissionMask_should_return_correct()
|
||||||
|
{
|
||||||
// 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("000").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("0000").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("100").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("0200").Should().BeFalse();
|
Subject.IsValidFolderPermissionMask("200").Should().BeFalse();
|
||||||
Subject.IsValidFilePermissionMask("0600").Should().BeTrue();
|
Subject.IsValidFolderPermissionMask("300").Should().BeFalse();
|
||||||
|
Subject.IsValidFolderPermissionMask("400").Should().BeFalse();
|
||||||
|
Subject.IsValidFolderPermissionMask("500").Should().BeFalse();
|
||||||
|
Subject.IsValidFolderPermissionMask("600").Should().BeFalse();
|
||||||
|
Subject.IsValidFolderPermissionMask("700").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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,15 +61,29 @@ public override void SetEveryonePermissions(string filename)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve existing non-access permissions unless mask is 4 digits
|
||||||
|
if (mask.Length < 4)
|
||||||
|
{
|
||||||
|
permissions |= curStat.st_mode & ~FilePermissions.ACCESSPERMS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Syscall.chmod(path, permissions) < 0)
|
if (Syscall.chmod(path, permissions) < 0)
|
||||||
@ -78,33 +92,39 @@ public override void SetPermissions(string path, string mask)
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.ConstrainedExecution;
|
using System.Runtime.ConstrainedExecution;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Permissions;
|
using System.Security.Permissions;
|
||||||
@ -25,13 +25,13 @@ public SafeUnixHandle(int fd)
|
|||||||
|
|
||||||
public override bool IsInvalid
|
public override bool IsInvalid
|
||||||
{
|
{
|
||||||
get { return this.handle == new IntPtr(-1); }
|
get { return handle == new IntPtr(-1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
||||||
protected override bool ReleaseHandle()
|
protected override bool ReleaseHandle()
|
||||||
{
|
{
|
||||||
return Syscall.close(this.handle.ToInt32()) != -1;
|
return Syscall.close(handle.ToInt32()) != -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
@ -131,7 +131,7 @@ public void Start(string installationFolder, int processId)
|
|||||||
// Set executable flag on app
|
// Set executable flag on app
|
||||||
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
|
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
|
||||||
{
|
{
|
||||||
_diskProvider.SetPermissions(Path.Combine(installationFolder, "Radarr"), "0755");
|
_diskProvider.SetPermissions(Path.Combine(installationFolder, "Radarr"), "755", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
@ -91,7 +91,7 @@ public override void SetEveryonePermissions(string filename)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetPermissions(string path, string mask)
|
public override void SetPermissions(string path, string mask, string group)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,11 +8,11 @@ namespace Radarr.Api.V3.Config
|
|||||||
{
|
{
|
||||||
public class MediaManagementConfigModule : RadarrConfigModule<MediaManagementConfigResource>
|
public class MediaManagementConfigModule : RadarrConfigModule<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) && (OsInfo.IsLinux || OsInfo.IsOsx));
|
SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && (OsInfo.IsLinux || OsInfo.IsOsx));
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,8 @@ public class MediaManagementConfigResource : RestResource
|
|||||||
public bool PathsDefaultStatic { get; set; }
|
public bool PathsDefaultStatic { 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 int MinimumFreeSpaceWhenImporting { get; set; }
|
public int MinimumFreeSpaceWhenImporting { get; set; }
|
||||||
@ -46,7 +47,8 @@ public static MediaManagementConfigResource ToResource(IConfigService model)
|
|||||||
AutoRenameFolders = model.AutoRenameFolders,
|
AutoRenameFolders = model.AutoRenameFolders,
|
||||||
|
|
||||||
SetPermissionsLinux = model.SetPermissionsLinux,
|
SetPermissionsLinux = model.SetPermissionsLinux,
|
||||||
FileChmod = model.FileChmod,
|
ChmodFolder = model.ChmodFolder,
|
||||||
|
ChownGroup = model.ChownGroup,
|
||||||
|
|
||||||
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
||||||
MinimumFreeSpaceWhenImporting = model.MinimumFreeSpaceWhenImporting,
|
MinimumFreeSpaceWhenImporting = model.MinimumFreeSpaceWhenImporting,
|
||||||
|
@ -28,7 +28,7 @@ protected ProviderModuleBase(IProviderFactory<TProvider, TProviderDefinition> pr
|
|||||||
Get("schema", x => GetTemplates());
|
Get("schema", x => GetTemplates());
|
||||||
Post("test", x => Test(ReadResourceFromRequest(true)));
|
Post("test", x => Test(ReadResourceFromRequest(true)));
|
||||||
Post("testall", x => TestAll());
|
Post("testall", x => TestAll());
|
||||||
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true)));
|
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true)));
|
||||||
|
|
||||||
GetResourceAll = GetAll;
|
GetResourceAll = GetAll;
|
||||||
GetResourceById = GetProviderById;
|
GetResourceById = GetProviderById;
|
||||||
|
@ -15,9 +15,9 @@ public class Field
|
|||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public bool Advanced { get; set; }
|
public bool Advanced { get; set; }
|
||||||
public List<SelectOption> SelectOptions { get; set; }
|
public List<SelectOption> SelectOptions { get; set; }
|
||||||
|
public string SelectOptionsProviderAction { get; set; }
|
||||||
public string Section { get; set; }
|
public string Section { get; set; }
|
||||||
public string Hidden { get; set; }
|
public string Hidden { get; set; }
|
||||||
public string RequestAction { get; set; }
|
|
||||||
|
|
||||||
public Field Clone()
|
public Field Clone()
|
||||||
{
|
{
|
||||||
|
@ -100,13 +100,19 @@ private static FieldMapping[] GetFieldMapping(Type type, string prefix, Func<obj
|
|||||||
Order = fieldAttribute.Order,
|
Order = fieldAttribute.Order,
|
||||||
Advanced = fieldAttribute.Advanced,
|
Advanced = fieldAttribute.Advanced,
|
||||||
Type = fieldAttribute.Type.ToString().FirstCharToLower(),
|
Type = fieldAttribute.Type.ToString().FirstCharToLower(),
|
||||||
Section = fieldAttribute.Section,
|
Section = fieldAttribute.Section
|
||||||
RequestAction = fieldAttribute.RequestAction
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fieldAttribute.Type == FieldType.Select || fieldAttribute.Type == FieldType.TagSelect)
|
if (fieldAttribute.Type == FieldType.Select || fieldAttribute.Type == FieldType.TagSelect)
|
||||||
{
|
{
|
||||||
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
|
if (fieldAttribute.SelectOptionsProviderAction.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
field.SelectOptionsProviderAction = fieldAttribute.SelectOptionsProviderAction;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldAttribute.Hidden != HiddenType.Visible)
|
if (fieldAttribute.Hidden != HiddenType.Visible)
|
||||||
@ -215,7 +221,11 @@ private static Func<object, object> GetValueConverter(Type propertyType)
|
|||||||
{
|
{
|
||||||
return fieldValue =>
|
return fieldValue =>
|
||||||
{
|
{
|
||||||
if (fieldValue.GetType() == typeof(JArray))
|
if (fieldValue == null)
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<int>();
|
||||||
|
}
|
||||||
|
else if (fieldValue.GetType() == typeof(JArray))
|
||||||
{
|
{
|
||||||
return ((JArray)fieldValue).Select(s => s.Value<int>());
|
return ((JArray)fieldValue).Select(s => s.Value<int>());
|
||||||
}
|
}
|
||||||
@ -229,7 +239,11 @@ private static Func<object, object> GetValueConverter(Type propertyType)
|
|||||||
{
|
{
|
||||||
return fieldValue =>
|
return fieldValue =>
|
||||||
{
|
{
|
||||||
if (fieldValue.GetType() == typeof(JArray))
|
if (fieldValue == null)
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<string>();
|
||||||
|
}
|
||||||
|
else if (fieldValue.GetType() == typeof(JArray))
|
||||||
{
|
{
|
||||||
return ((JArray)fieldValue).Select(s => s.Value<string>());
|
return ((JArray)fieldValue).Select(s => s.Value<string>());
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using Nancy.Responses.Negotiation;
|
using Nancy.Responses.Negotiation;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@ -224,7 +225,7 @@ protected Negotiator ResponseWithCode(object model, HttpStatusCode statusCode)
|
|||||||
return Negotiate.WithModel(model).WithStatusCode(statusCode);
|
return Negotiate.WithModel(model).WithStatusCode(statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected TResource ReadResourceFromRequest(bool skipValidate = false)
|
protected TResource ReadResourceFromRequest(bool skipValidate = false, bool skipSharedValidate = false)
|
||||||
{
|
{
|
||||||
TResource resource;
|
TResource resource;
|
||||||
|
|
||||||
@ -242,7 +243,12 @@ protected TResource ReadResourceFromRequest(bool skipValidate = false)
|
|||||||
throw new BadRequestException("Request body can't be empty");
|
throw new BadRequestException("Request body can't be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
var errors = SharedValidator.Validate(resource).Errors.ToList();
|
var errors = new List<ValidationFailure>();
|
||||||
|
|
||||||
|
if (!skipSharedValidate)
|
||||||
|
{
|
||||||
|
errors.AddRange(SharedValidator.Validate(resource).Errors);
|
||||||
|
}
|
||||||
|
|
||||||
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase))
|
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user