mirror of
https://github.com/kopia/kopia.git
synced 2026-04-30 19:03:38 -04:00
The new files policy oneFileSystem ignores files that are mounted to other filesystems similarly to tar's --one-file-system switch. For example, if this is enabled, backing up / should now automatically ignore /dev, /proc, etc, so the directory entries themselves don't appear in the backup. The value of the policy is 'false' by default. This is implemented by adding a non-windows-field Device (of type DeviceInfo, reflecting the implementation of Owner) to the Entry interface. DeviceInfo holds the dev and rdev acquired with stat (same way as with Owner), but in addition to that it also holds the same values for the parent directory. It would seem that doing this in some other way, ie. in ReadDir, would require modifying the ReadDir interface which seems a too large modification for a feature this small. This change introduces a duplication of 'stat' call to the files, as the Owner feature already does a separate call. I doubt the performance implications are noticeable, though with some refactoring both Owner and Device fields could be filled in in one go. Filling in the field has been placed in fs/localfs/localfs.go where entryFromChildFileInfo has acquired a third parameter giving the the parent entry. From that information the Device of the parent is retrieved, to be passed off to platformSpecificDeviceInfo which does the rest of the paperwork. Other fs implementations just put in the default values. The Dev and Rdev fields returned by the 'stat' call have different sizes on different platforms, but for convenience they are internally handled the same. The conversion is done with local_fs_32bit.go and local_fs_64bit.go which are conditionally compiled on different platforms. Finally the actual check of the condition is in ignorefs.go function shouldIncludeByDevice which is analoguous to the other similarly named functions. Co-authored-by: Erkki Seppälä <flux@inside.org>
248 lines
11 KiB
JavaScript
248 lines
11 KiB
JavaScript
import axios from 'axios';
|
|
import React, { Component } from 'react';
|
|
import Button from 'react-bootstrap/Button';
|
|
import Col from 'react-bootstrap/Col';
|
|
import Form from 'react-bootstrap/Form';
|
|
import Tab from 'react-bootstrap/Tab';
|
|
import Tabs from 'react-bootstrap/Tabs';
|
|
import { handleChange, OptionalBoolean, OptionalNumberField, RequiredBoolean, stateProperty, StringList } from './forms';
|
|
|
|
function sourceDisplayName(s) {
|
|
if (!s.host && !s.userName) {
|
|
return "global"
|
|
}
|
|
|
|
if (!s.userName) {
|
|
return "host " + s.host;
|
|
}
|
|
|
|
if (!s.path) {
|
|
return "user " + s.userName + "@" + s.host;
|
|
}
|
|
|
|
return "user " + s.userName + "@" + s.host + " path '" + s.path + "'";
|
|
}
|
|
|
|
export class PolicyEditor extends Component {
|
|
constructor() {
|
|
super();
|
|
this.state = {
|
|
items: [],
|
|
isLoading: false,
|
|
error: null,
|
|
};
|
|
|
|
this.fetchPolicy = this.fetchPolicy.bind(this);
|
|
this.handleChange = handleChange.bind(this);
|
|
this.saveChanges = this.saveChanges.bind(this);
|
|
this.isGlobal = this.isGlobal.bind(this);
|
|
this.deletePolicy = this.deletePolicy.bind(this);
|
|
this.snapshotURL = this.snapshotURL.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
axios.get('/api/v1/repo/algorithms').then(result => {
|
|
this.setState({
|
|
algorithms: result.data,
|
|
});
|
|
|
|
this.fetchPolicy(this.props);
|
|
});
|
|
}
|
|
|
|
componentWillReceiveProps(props) {
|
|
this.fetchPolicy(props);
|
|
}
|
|
|
|
fetchPolicy(props) {
|
|
if (props.isNew) {
|
|
this.setState({
|
|
isLoading: false,
|
|
policy: {},
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
axios.get(this.snapshotURL(props)).then(result => {
|
|
this.setState({
|
|
isLoading: false,
|
|
policy: result.data,
|
|
});
|
|
}).catch(error => this.setState({
|
|
error: error,
|
|
isLoading: false
|
|
}));
|
|
}
|
|
|
|
saveChanges() {
|
|
function removeEmpty(l) {
|
|
if (!l) {
|
|
return l;
|
|
}
|
|
|
|
let result = [];
|
|
for (let i = 0; i < l.length; i++) {
|
|
const s = l[i];
|
|
if (s === "") {
|
|
continue;
|
|
}
|
|
|
|
result.push(s);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// clean up policy before saving
|
|
let policy = JSON.parse(JSON.stringify(this.state.policy));
|
|
if (policy.files) {
|
|
if (policy.files.ignore) {
|
|
policy.files.ignore = removeEmpty(policy.files.ignore)
|
|
}
|
|
if (policy.files.ignoreDotFiles) {
|
|
policy.files.ignoreDotFiles = removeEmpty(policy.files.ignoreDotFiles)
|
|
}
|
|
}
|
|
|
|
if (policy.compression) {
|
|
if (policy.compression.onlyCompress) {
|
|
policy.compression.onlyCompress = removeEmpty(policy.compression.onlyCompress)
|
|
}
|
|
if (policy.compression.neverCompress) {
|
|
policy.compression.neverCompress = removeEmpty(policy.compression.neverCompress)
|
|
}
|
|
}
|
|
|
|
axios.put(this.snapshotURL(this.props), policy).then(result => {
|
|
this.props.close();
|
|
}).catch(error => {
|
|
alert('Error saving snapshot: ' + error);
|
|
});
|
|
}
|
|
|
|
deletePolicy() {
|
|
if (window.confirm('Are you sure you want to delete this policy?')) {
|
|
axios.delete(this.snapshotURL(this.props)).then(result => {
|
|
this.props.close();
|
|
}).catch(error => {
|
|
alert('Delete error: ' + error);
|
|
});
|
|
}
|
|
}
|
|
|
|
snapshotURL(props) {
|
|
return '/api/v1/policy?host=' + props.host + '&userName=' + props.userName + '&path=' + props.path;
|
|
}
|
|
|
|
isGlobal() {
|
|
return !this.props.host && !this.props.userName && !this.props.path;
|
|
}
|
|
|
|
render() {
|
|
const { isLoading, error } = this.state;
|
|
if (error) {
|
|
return <p>{error.message}</p>;
|
|
}
|
|
|
|
if (isLoading) {
|
|
return <p>Loading ...</p>;
|
|
}
|
|
|
|
return <div className="padded">
|
|
<h3>{this.props.isNew && "New "}Policy: {sourceDisplayName(this.props)}</h3>
|
|
|
|
<Tabs defaultActiveKey="retention">
|
|
<Tab eventKey="retention" title="Retention">
|
|
<div className="tab-body">
|
|
<p className="policy-help">Controls how many latest snapshots to keep per source directory</p>
|
|
<Form.Row>
|
|
{OptionalNumberField(this, "Latest", "policy.retention.keepLatest", { placeholder: "# of latest snapshots" })}
|
|
{OptionalNumberField(this, "Hourly", "policy.retention.keepHourly", { placeholder: "# of hourly snapshots" })}
|
|
{OptionalNumberField(this, "Daily", "policy.retention.keepDaily", { placeholder: "# of daily snapshots" })}
|
|
</Form.Row>
|
|
<Form.Row>
|
|
{OptionalNumberField(this, "Weekly", "policy.retention.keepWeekly", { placeholder: "# of weekly snapshots" })}
|
|
{OptionalNumberField(this, "Monthly", "policy.retention.keepMonthly", { placeholder: "# of monthly snapshots" })}
|
|
{OptionalNumberField(this, "Annual", "policy.retention.keepAnnual", { placeholder: "# of annual snapshots" })}
|
|
</Form.Row>
|
|
</div>
|
|
</Tab>
|
|
<Tab eventKey="files" title="Files">
|
|
<div className="tab-body">
|
|
<p className="policy-help">Controls which files should be included and excluded when snapshotting. Use <a href="https://git-scm.com/docs/gitignore">.gitignore</a> syntax.</p>
|
|
<Form.Row>
|
|
{StringList(this, "Ignore Rules", "policy.files.ignore", "List of file name patterns to ignore.")}
|
|
{StringList(this, "Ignore Rule Files", "policy.files.ignoreDotFiles", "List of additional files containing ignore rules. Each file configures ignore rules for the directory and its subdirectories.")}
|
|
</Form.Row>
|
|
<Form.Row>
|
|
{RequiredBoolean(this, "Ignore Parent Rules", "policy.files.noParentIgnore")}
|
|
{RequiredBoolean(this, "Ignore Parent Rule Files", "policy.files.noParentDotFiles")}
|
|
</Form.Row>
|
|
<Form.Row>
|
|
{OptionalBoolean(this, "Ignore Well-Known Cache Directories", "policy.files.ignoreCacheDirs", "inherit from parent")}
|
|
</Form.Row>
|
|
<Form.Row>
|
|
{OptionalBoolean(this, "Scan only one filesystem", "policy.files.oneFileSystem", "inherit from parent")}
|
|
</Form.Row>
|
|
</div>
|
|
</Tab>
|
|
<Tab eventKey="errors" title="Errors">
|
|
<div class="tab-body">
|
|
<p className="policy-help">Controls how errors detected while snapshotting are handled.</p>
|
|
<Form.Row>
|
|
{OptionalBoolean(this, "Ignore Directory Errors", "policy.errorHandling.ignoreDirectoryErrors", "inherit from parent")}
|
|
{OptionalBoolean(this, "Ignore File Errors", "policy.errorHandling.ignoreFileErrors", "inherit from parent")}
|
|
</Form.Row>
|
|
</div>
|
|
</Tab>
|
|
<Tab eventKey="compression" title="Compression">
|
|
<div class="tab-body">
|
|
<p className="policy-help">Controls which files are compressed.</p>
|
|
<Form.Row>
|
|
<Form.Group as={Col}>
|
|
<Form.Label className="required">Compression</Form.Label>
|
|
<Form.Control as="select"
|
|
name="policy.compression.compressorName"
|
|
onChange={this.handleChange}
|
|
value={stateProperty(this, "policy.compression.compressorName")}>
|
|
<option value="">(none)</option>
|
|
{this.state.algorithms && this.state.algorithms.compression.map(x => <option key={x} value={x}>{x}</option>)}
|
|
</Form.Control>
|
|
</Form.Group>
|
|
{OptionalNumberField(this, "Min File Size", "policy.compression.minSize", { placeholder: "minimum file size to compress" })}
|
|
{OptionalNumberField(this, "Max File Size", "policy.compression.maxSize", { placeholder: "maximum file size to compress" })}
|
|
</Form.Row>
|
|
<Form.Row>
|
|
{StringList(this, "Only Compress Extensions", "policy.compression.onlyCompress", "Only compress files with the above file extensions (one extension per line)")}
|
|
{StringList(this, "Never Compress Extensions", "policy.compression.neverCompress", "Never compress the above file extensions (one extension per line)")}
|
|
</Form.Row>
|
|
</div>
|
|
</Tab>
|
|
<Tab eventKey="scheduling" title="Scheduling">
|
|
<div class="tab-body">
|
|
<p className="policy-help">Controls when snapshots are automatically created.</p>
|
|
<Form.Row>
|
|
{OptionalNumberField(this, "Snapshot Interval", "policy.scheduling.intervalSeconds", { placeholder: "seconds" })}
|
|
</Form.Row>
|
|
</div>
|
|
</Tab>
|
|
</Tabs>
|
|
<Form.Row>
|
|
{RequiredBoolean(this, "Disable Parent Policy Evaluation (prevents any parent policies from affecting this directory and subdirectories)", "policy.noParent")}
|
|
</Form.Row>
|
|
|
|
<Button variant="success" onClick={this.saveChanges}>Save Policy</Button>
|
|
{!this.props.isNew && <>
|
|
<Button variant="danger" disabled={this.isGlobal()} onClick={this.deletePolicy}>Delete Policy</Button>
|
|
</>}
|
|
|
|
<Button variant="dark" onClick={this.props.close}>Back To List</Button>
|
|
<hr />
|
|
<h5>JSON representation</h5>
|
|
<pre className="debug-json">{JSON.stringify(this.state.policy, null, 4)}
|
|
</pre>
|
|
</div>;
|
|
}
|
|
}
|