New: Displaying folder-based permissions in UI rather than file-based permissions and with selectable sane presets
Fixed: Preserve setgid when applying unix permissionspull/4111/head
parent
850552bf17
commit
d88bb7f855
@ -0,0 +1,53 @@
|
|||||||
|
.inputWrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputFolder {
|
||||||
|
composes: input from '~Components/Form/Input.css';
|
||||||
|
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputUnitWrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputUnit {
|
||||||
|
composes: inputUnit from '~Components/Form/FormInputGroup.css';
|
||||||
|
|
||||||
|
right: 40px;
|
||||||
|
font-family: $monoSpaceFontFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-family: $monoSpaceFontFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: 17px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 0 0 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
width: 90px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.readOnly {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import styles from './UMaskInput.css';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
const umaskOptions = [
|
||||||
|
{
|
||||||
|
key: '755',
|
||||||
|
value: '755 - Owner write, Everyone else read',
|
||||||
|
hint: 'drwxr-xr-x'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '775',
|
||||||
|
value: '775 - Owner & Group write, Other read',
|
||||||
|
hint: 'drwxrwxr-x'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '770',
|
||||||
|
value: '770 - Owner & Group write',
|
||||||
|
hint: 'drwxrwx---'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '750',
|
||||||
|
value: '750 - Owner write, Group read',
|
||||||
|
hint: 'drwxr-x---'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '777',
|
||||||
|
value: '777 - Everyone write',
|
||||||
|
hint: 'drwxrwxrwx'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function formatPermissions(permissions) {
|
||||||
|
|
||||||
|
const hasSticky = permissions & 0o1000;
|
||||||
|
const hasSetGID = permissions & 0o2000;
|
||||||
|
const hasSetUID = permissions & 0o4000;
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
const bit = (permissions & (1 << i)) !== 0;
|
||||||
|
let digit = bit ? 'xwr'[i % 3] : '-';
|
||||||
|
if (i === 6 && hasSetUID) {
|
||||||
|
digit = bit ? 's' : 'S';
|
||||||
|
} else if (i === 3 && hasSetGID) {
|
||||||
|
digit = bit ? 's' : 'S';
|
||||||
|
} else if (i === 0 && hasSticky) {
|
||||||
|
digit = bit ? 't' : 'T';
|
||||||
|
}
|
||||||
|
result = digit + result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UMaskInput extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const valueNum = parseInt(value, 8);
|
||||||
|
const umaskNum = 0o777 & ~valueNum;
|
||||||
|
const umask = umaskNum.toString(8).padStart(4, '0');
|
||||||
|
const folderNum = 0o777 & ~umaskNum;
|
||||||
|
const folder = folderNum.toString(8).padStart(3, '0');
|
||||||
|
const fileNum = 0o666 & ~umaskNum;
|
||||||
|
const file = fileNum.toString(8).padStart(3, '0');
|
||||||
|
|
||||||
|
const unit = formatPermissions(folderNum);
|
||||||
|
|
||||||
|
const values = umaskOptions.map((v) => {
|
||||||
|
return { ...v, hint: <span className={styles.unit}>{v.hint}</span> };
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<div className={styles.inputUnitWrapper}>
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
values={values}
|
||||||
|
isEditable={true}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.inputUnit}>
|
||||||
|
d{unit}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.details}>
|
||||||
|
<div>
|
||||||
|
<label>UMask</label>
|
||||||
|
<div className={styles.value}>{umask}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Folder</label>
|
||||||
|
<div className={styles.value}>{folder}</div>
|
||||||
|
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>File</label>
|
||||||
|
<div className={styles.value}>{file}</div>
|
||||||
|
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UMaskInput.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onFocus: PropTypes.func,
|
||||||
|
onBlur: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UMaskInput;
|
@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(147)]
|
||||||
|
public class swap_filechmod_for_folderchmod : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
// Reverts part of migration 140, note that the v1 of migration140 also removed chowngroup
|
||||||
|
Execute.WithConnection(ConvertFileChmodToFolderChmod);
|
||||||
|
}
|
||||||
|
private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
using (IDbCommand getFileChmodCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
getFileChmodCmd.Transaction = tran;
|
||||||
|
getFileChmodCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'filechmod'";
|
||||||
|
|
||||||
|
var fileChmod = getFileChmodCmd.ExecuteScalar() as string;
|
||||||
|
if (fileChmod != null)
|
||||||
|
{
|
||||||
|
if (fileChmod.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
// Convert without using mono libraries. We take the 'r' bits and shifting them to the 'x' position, preserving everything else.
|
||||||
|
var fileChmodNum = Convert.ToInt32(fileChmod, 8);
|
||||||
|
var folderChmodNum = fileChmodNum | ((fileChmodNum & 0x124) >> 2);
|
||||||
|
var folderChmod = Convert.ToString(folderChmodNum, 8).PadLeft(3, '0');
|
||||||
|
|
||||||
|
using (IDbCommand insertCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
insertCmd.Transaction = tran;
|
||||||
|
insertCmd.CommandText = "INSERT INTO Config (Key, Value) VALUES ('chmodfolder', ?)";
|
||||||
|
insertCmd.AddParameter(folderChmod);
|
||||||
|
|
||||||
|
insertCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (IDbCommand deleteCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
deleteCmd.Transaction = tran;
|
||||||
|
deleteCmd.CommandText = "DELETE FROM Config WHERE Key = 'filechmod'";
|
||||||
|
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue