htmlui: added experimental HTML-based UI

This is enabled by `kopia server --ui` and can be viewed in a browser
at http://localhost:51515/

Right now it can only list snapshots and policies (barely).
This commit is contained in:
Jarek Kowalski
2019-11-07 23:01:45 -08:00
parent 945708c5c5
commit e8a1eaa9ee
33 changed files with 15128 additions and 51 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ test_service_account.json
id_kopia
known_hosts
node_modules/
*_bindata.go

View File

@@ -1,5 +1,8 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
before:
hooks:
- make html-ui-bindata
builds:
- env:
- CGO_ENABLED=0
@@ -11,6 +14,8 @@ builds:
- amd64
- arm
- arm64
flags:
- -tags=embedhtml
ldflags:
- -s -w -X "github.com/kopia/kopia/repo.BuildVersion={{.Version}}" -X "github.com/kopia/kopia/repo.BuildInfo={{.Commit}}"
archives:

View File

@@ -1,11 +1,7 @@
COVERAGE_PACKAGES=./repo/...,./fs/...,./snapshot/...
GOLANGCI_LINT_VERSION=v1.18.0
LINTER_TOOL=.tools/bin/golangci-lint
GOVERALLS_TOOL=.tools/bin/goveralls
GO_TEST=go test
NODE_VERSION=12.13.0
TOOLS_DIR=$(CURDIR)/.tools
NPM_TOOL=$(TOOLS_DIR)/nodejs/node/bin/npm
include tools/tools.mk
-include ./Makefile.local.mk
@@ -43,19 +39,20 @@ build-linux-arm64:
build-all: build-linux-amd64 build-windows-amd64 build-darwin-amd64 build-linux-arm build-linux-arm64
$(LINTER_TOOL):
mkdir -p .tools
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b .tools/bin/ $(GOLANGCI_LINT_VERSION)
$(GOVERALLS_TOOL):
mkdir -p .tools
GO111MODULE=off GOPATH=$(CURDIR)/.tools go get github.com/mattn/goveralls
travis-setup: travis-install-gpg-key travis-install-test-credentials
go mod download
website:
$(MAKE) -C site
$(MAKE) -C site build
html-ui:
$(MAKE) -C htmlui build-html
html-ui-bindata: html-ui $(BINDATA_TOOL)
(cd htmlui/build && $(BINDATA_TOOL) -fs -tags embedhtml -o "$(CURDIR)/internal/server/htmlui_bindata.go" -pkg server -ignore '.map' . static/css static/js static/media)
html-ui-bindata-fallback: $(BINDATA_TOOL)
(cd internal/server && $(BINDATA_TOOL) -fs -tags !embedhtml -o "$(CURDIR)/internal/server/htmlui_fallback.go" -pkg server index.html)
travis-release: test-with-coverage lint vet verify-release integration-tests upload-coverage website stress-test
@@ -169,14 +166,4 @@ travis-create-long-term-repository:
endif
site/node_modules: install-webtools
install-webtools:
mkdir -p $(TOOLS_DIR)/nodejs
ifeq ($(uname),Linux)
curl -LsS https://nodejs.org/dist/v$(NODE_VERSION)/node-v$(NODE_VERSION)-linux-x64.tar.gz | tar zx -C $(TOOLS_DIR)/nodejs
else
curl -LsS https://nodejs.org/dist/v$(NODE_VERSION)/node-v$(NODE_VERSION)-darwin-x64.tar.gz | tar zx -C $(TOOLS_DIR)/nodejs
endif
mv $(TOOLS_DIR)/nodejs/node-v$(NODE_VERSION)* $(TOOLS_DIR)/nodejs/node/

View File

@@ -16,6 +16,7 @@
serverStartCommand = serverCommands.Command("start", "Start Kopia server").Default()
serverStartHTMLPath = serverStartCommand.Flag("html", "Server the provided HTML at the root URL").ExistingDir()
serverStartUI = serverStartCommand.Flag("ui", "Start the server with HTML UI (EXPERIMENTAL)").Bool()
)
func init() {
@@ -36,6 +37,8 @@ func runServer(ctx context.Context, rep *repo.Repository) error {
if *serverStartHTMLPath != "" {
fileServer := http.FileServer(http.Dir(*serverStartHTMLPath))
http.Handle("/", fileServer)
} else if *serverStartUI {
http.Handle("/", http.FileServer(server.AssetFile()))
}
return http.ListenAndServe(*serverAddress, nil)
}

1
go.mod
View File

@@ -12,6 +12,7 @@ require (
github.com/chmduquesne/rollinghash v4.0.0+incompatible
github.com/danieljoos/wincred v1.0.2 // indirect
github.com/efarrer/iothrottler v0.0.1
github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect
github.com/go-ini/ini v1.46.0 // indirect
github.com/godbus/dbus v4.1.0+incompatible // indirect
github.com/kylelemons/godebug v1.1.0

2
go.sum
View File

@@ -36,6 +36,8 @@ github.com/efarrer/iothrottler v0.0.0-20141121142253-60e7e547c7fe h1:WAx1vRufH0I
github.com/efarrer/iothrottler v0.0.0-20141121142253-60e7e547c7fe/go.mod h1:zjXkUoNEq44qYz/1TlzBhN2W21rDU3HvDBiJWQAZTq8=
github.com/efarrer/iothrottler v0.0.1 h1:N5uXoCpk8T1nfl8z7l4YIJUI/2/mL5pQNsOkeMuVnH8=
github.com/efarrer/iothrottler v0.0.1/go.mod h1:zGWF5N0NKSCskcPFytDAFwI121DdU/NfW4XOjpTR+ys=
github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE=
github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-ini/ini v1.42.0 h1:TWr1wGj35+UiWHlBA8er89seFXxzwFn11spilrrj+38=
github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-ini/ini v1.46.0 h1:hDJFfs/9f75875scvqLkhNB5Jz5/DybKEOZ5MLF+ng4=

2
htmlui/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
build/

12
htmlui/Makefile Normal file
View File

@@ -0,0 +1,12 @@
all:
include ../tools/tools.mk
dev: node_modules
PATH=$(TOOL_PATH) $(NPM_TOOL) run start
build-html: node_modules
PATH=$(TOOL_PATH) $(NPM_TOOL) run build
node_modules: $(NPM_TOOL)
PATH=$(TOOL_PATH) $(NPM_TOOL) install

68
htmlui/README.md Normal file
View File

@@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

1
htmlui/env.sh Normal file
View File

@@ -0,0 +1 @@
PATH=$(pwd)/../.tools/nodejs/node/bin:$PATH

13816
htmlui/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

37
htmlui/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "htmlui",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"react": "^16.11.0",
"react-bootstrap": "^1.0.0-beta.14",
"react-dom": "^16.11.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0",
"react-table": "^6.10.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"proxy": "http://localhost:51515",
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
htmlui/public/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

43
htmlui/public/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Kopia UI</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
htmlui/public/logo192.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
htmlui/public/logo512.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

2
htmlui/public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

7
htmlui/src/App.css Normal file
View File

@@ -0,0 +1,7 @@
.App-logo {
height: 32px;
}
.active{
font-weight: bold;
}

67
htmlui/src/App.js Normal file
View File

@@ -0,0 +1,67 @@
import React from 'react';
import logo from './kopia-flat.svg';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-table/react-table.css'
import { SourcesTable } from "./SourcesTable";
import { PoliciesTable } from "./PoliciesTable";
import { SnapshotsTable } from "./SnapshotsTable";
import Navbar from 'react-bootstrap/Navbar';
import { NavLink } from 'react-router-dom';
import Nav from 'react-bootstrap/Nav';
import Container from 'react-bootstrap/Container';
import { withRouter } from "react-router";
import {
BrowserRouter as Router,
Switch,
Route,
} from "react-router-dom";
const SnapshotsTableWithRouter = withRouter(SnapshotsTable);
function App() {
return (
<Router>
<Navbar bg="light" expand="lg">
<Navbar.Brand href="/"><img src={logo} className="App-logo" alt="logo" /></Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<NavLink className="nav-link" activeClassName="active" exact to="/">Status</NavLink>
<NavLink className="nav-link" activeClassName="active" to="/snapshots">Snapshots</NavLink>
<NavLink className="nav-link" activeClassName="active" to="/policies">Policies</NavLink>
</Nav>
</Navbar.Collapse>
</Navbar>
<Container>
<Switch>
<Route path="/snapshots/single-source/">
<SnapshotsTableWithRouter />
</Route>
<Route path="/snapshots/dir/">
<p>not implemented: directory browser</p>
</Route>
<Route path="/snapshots/file/">
<p>not implemented: file browser</p>
</Route>
<Route path="/snapshots">
<SourcesTable />
</Route>
<Route path="/policies">
<PoliciesTable />
</Route>
<Route exact path="/">
<p>not implemented: Status</p>
</Route>
</Switch>
</Container>
</Router>
);
}
export default App;

9
htmlui/src/App.test.js Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@@ -0,0 +1,77 @@
import React, { Component } from 'react';
import ReactTable from 'react-table';
import Button from 'react-bootstrap/Button';
import axios from 'axios';
import {
sourceDisplayName,
intervalDisplayName,
timesOfDayDisplayName,
} from './uiutil';
export class PoliciesTable extends Component {
constructor() {
super();
this.state = {
items: [],
isLoading: false,
error: null,
};
}
;
componentDidMount() {
axios.get('/api/v1/policies').then(result => {
this.setState({ "items": result.data.policies });
}).catch(error => this.setState({
error,
isLoading: false
}));
}
render() {
const { items, isLoading, error } = this.state;
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <p>Loading ...</p>;
}
const columns = [{
id: 'target',
Header: 'Target',
accessor: x => sourceDisplayName(x.target),
}, {
Header: 'Latest',
accessor: 'policy.retention.keepLatest'
}, {
Header: 'Hourly',
accessor: 'policy.retention.keepHourly'
}, {
Header: 'Daily',
accessor: 'policy.retention.keepDaily'
}, {
Header: 'Weekly',
accessor: 'policy.retention.keepWeekly'
}, {
Header: 'Monthly',
accessor: 'policy.retention.keepMonthly'
}, {
Header: 'Annual',
accessor: 'policy.retention.keepAnnual'
}, {
id: 'interval',
Header: 'Interval',
accessor: x => intervalDisplayName(x.policy.scheduling.interval),
}, {
id: 'timesOfDay',
Header: 'Times of Day',
accessor: x => timesOfDayDisplayName(x.policy.scheduling.timesOfDay),
}]
return <div>
<Button size="xxl">
flat button
</Button>
<ReactTable data={items} columns={columns} />;
</div>
}
}

View File

@@ -0,0 +1,163 @@
import React, { Component } from 'react';
import ReactTable from 'react-table';
import axios from 'axios';
import {
parseQuery,
sizeDisplayName,
objectLink,
rfc3339TimestampDisplayName,
} from './uiutil';
import {
Link
} from "react-router-dom";
import Spinner from 'react-bootstrap/Spinner';
import Badge from 'react-bootstrap/Badge';
import Form from 'react-bootstrap/Form';
function pillVariant(tag) {
if (tag.startsWith("latest-")) {
return "success";
}
if (tag.startsWith("daily-")) {
return "info";
}
if (tag.startsWith("weekly-")) {
return "danger";
}
if (tag.startsWith("monthly-")) {
return "dark";
}
if (tag.startsWith("annual-")) {
return "warning";
}
return "primary";
}
export class SnapshotsTable extends Component {
constructor() {
super();
this.state = {
snapshots: [],
isLoading: false,
error: null,
};
}
componentDidMount() {
let q = parseQuery(this.props.location.search);
this.setState({
isLoading: true,
host: q.host,
userName: q.userName,
path: q.path,
hiddenCount: 0,
selectedSnapshot: null,
});
const u = '/api/v1/snapshots?host=' + q.host + '&userName=' + q.userName + '&path=' + q.path;
console.log('u', u);
axios.get(u).then(result => {
console.log('got snapshots', result.data);
this.setState({
snapshots: result.data.snapshots,
isLoading: false,
});
}).catch(error => this.setState({
error,
isLoading: false
}));
}
coalesceSnapshots(s) {
let filteredSnapshots = [];
let lastRootID = "";
let hiddenCount = 0;
for (let i = 0; i < s.length; i++) {
if (s[i].rootID !== lastRootID) {
filteredSnapshots.push(s[i]);
} else {
hiddenCount++;
}
lastRootID = s[i].rootID;
}
return { filteredSnapshots, hiddenCount };
}
selectSnapshot(x) {
this.setState({
selectedSnapshot: x,
})
}
render() {
let { snapshots, isLoading, error } = this.state;
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <Spinner animation="border" variant="primary" />;
}
snapshots.sort((a,b)=> {
if (a.startTime < b.startTime) { return 1; };
if (a.startTime > b.startTime) { return -1; };
return 0;
});
let { filteredSnapshots, hiddenCount } = this.coalesceSnapshots(snapshots);
const columns = [{
Header: 'Start time',
accessor: 'startTime',
width: 200,
Cell: x => rfc3339TimestampDisplayName(x.value),
}, {
Header: 'Size',
accessor: 'summary.size',
width: 100,
Cell: x => sizeDisplayName(x.value),
}, {
Header: 'Files',
accessor: 'summary.files',
width: 100,
}, {
Header: 'Dirs',
accessor: 'summary.dirs',
width: 100,
}, {
id: 'rootID',
Header: 'Root',
accessor: x => <Link to={objectLink(x.rootID)}>{x.rootID}</Link>,
width: 300,
}, {
Header: 'Retention',
accessor: 'retention',
Cell: x => <span>{x.value.map(l =>
<Badge variant={pillVariant(l)}>{l}</Badge>
)}</span>
}]
return <div>
<Form>
<Form.Group controlId="formBasicCheckbox">
<Form.Label>Displaying {filteredSnapshots.length} snapshots of <b>{this.state.userName}@{this.state.host}:{this.state.path}</b></Form.Label>
</Form.Group>
<Form.Group controlId="formBasicCheckbox">
<Form.Check type="checkbox" label={'Show ' + hiddenCount + ' hidden snapshots'} />
</Form.Group>
{this.state.selectedSnapshot ?
<Form.Group controlId="formSelected">
<Form.Label>Selected <pre>{JSON.stringify(this.state.selectedSnapshot, null, 2)}</pre></Form.Label>
</Form.Group> : null}
</Form>
<ReactTable data={filteredSnapshots} columns={columns} />
</div>;
}
}

147
htmlui/src/SourcesTable.js Normal file
View File

@@ -0,0 +1,147 @@
import React, { Component } from 'react';
import ReactTable from 'react-table';
import axios from 'axios';
import {
Link
} from 'react-router-dom';
import Dropdown from 'react-bootstrap/Dropdown';
import Spinner from 'react-bootstrap/Spinner';
import Row from 'react-bootstrap/Row';
const allHosts = "(all)"
const allUsers = "(all)"
export class SourcesTable extends Component {
constructor() {
super();
this.state = {
sources: [],
isLoading: false,
error: null,
selectedHost: allHosts,
selectedUser: allUsers,
};
}
;
componentDidMount() {
this.setState({ isLoading: true });
axios.get('/api/v1/sources').then(result => {
this.setState({
sources: result.data.sources,
isLoading: false,
});
}).catch(error => this.setState({
error,
isLoading: false
}));
}
selectHost(h) {
this.setState({
selectedHost: h,
});
}
selectUser(u) {
this.setState({
selectedUser: u,
});
}
hostClicked(h) {
alert('host clicked ' + h);
}
render() {
let { sources, isLoading, error } = this.state;
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <Spinner animation="border" variant="primary" />;
}
let uniqueHosts = sources.reduce((a, d) => {
if (!a.includes(d.source.host)) { a.push(d.source.host); }
return a;
}, []);
uniqueHosts.sort();
let uniqueUsers = sources.reduce((a, d) => {
if (!a.includes(d.source.userName)) { a.push(d.source.userName); }
return a;
}, []);
uniqueUsers.sort();
if (this.state.selectedHost !== allHosts) {
sources = sources.filter(x => x.source.host === this.state.selectedHost);
};
if (this.state.selectedUser !== allUsers) {
sources = sources.filter(x => x.source.userName === this.state.selectedUser);
};
const columns = [{
id: 'host',
Header: 'Host',
accessor: 'source.host',
width: 150,
}, {
id: 'user',
Header: 'User',
accessor: 'source.userName',
width: 150,
}, {
id: 'path',
Header: 'Path',
accessor: x => x.source,
Cell: x => <Link to={'/snapshots/single-source?userName=' + x.value.userName + '&host=' + x.value.host + '&path=' + x.value.path}>{x.value.path}</Link>,
width: 600,
}, {
id: 'lastSnapshotTime',
Header: 'Last Snapshot',
width: 200,
accessor: x => x.lastSnapshotTime,
}, {
id: 'lastSnapshotSize',
Header: 'Size',
width: 100,
accessor: x => x.lastSnapshotSize,
}]
return <div>
<Row>
<Dropdown>
<Dropdown.Toggle variant="primary" id="dropdown-basic">
Host: {this.state.selectedHost}
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => this.selectHost(allHosts)}>(all)</Dropdown.Item>
{uniqueHosts.map(v => <Dropdown.Item onClick={() => this.selectHost(v)}>{v}</Dropdown.Item>)}
</Dropdown.Menu>
</Dropdown>
&nbsp;
<Dropdown>
<Dropdown.Toggle variant="primary" id="dropdown-basic">
User: {this.state.selectedUser}
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => this.selectUser(allUsers)}>(all)</Dropdown.Item>
{uniqueUsers.map(v => <Dropdown.Item onClick={() => this.selectUser(v)}>{v}</Dropdown.Item>)}
</Dropdown.Menu>
</Dropdown>
</Row>
<p></p>
<Row><ReactTable data={sources} columns={columns} /></Row>
</div>;
}
}

13
htmlui/src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

6
htmlui/src/index.js Normal file
View File

@@ -0,0 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

139
htmlui/src/kopia-flat.svg Normal file
View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="31"
viewBox="0 0 12.700008 8.2020885"
version="1.1"
id="svg4919"
inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
sodipodi:docname="kopia-flat.svg"
inkscape:export-filename="/Users/jarek/Projects/kopia-ui/resources/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs4913">
<linearGradient
osb:paint="gradient"
id="linearGradient4578">
<stop
id="stop4574"
offset="0"
style="stop-color:#000000;stop-opacity:1;" />
<stop
id="stop4576"
offset="1"
style="stop-color:#0066ff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient2345"
osb:paint="gradient">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2341" />
<stop
style="stop-color:#0066ff;stop-opacity:0;"
offset="1"
id="stop2343" />
</linearGradient>
</defs>
<sodipodi:namedview
inkscape:pagecheckerboard="false"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6571604"
inkscape:cx="53.451757"
inkscape:cy="30.205905"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="795"
inkscape:window-x="0"
inkscape:window-y="23"
inkscape:window-maximized="0"
units="px"
width="64px"
showguides="false"
inkscape:snap-text-baseline="true"
inkscape:document-rotation="0" />
<metadata
id="metadata4916">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-280.06662)">
<g
id="g849"
transform="matrix(0.04649709,0,0,0.04649709,-0.02134367,277.52719)"
inkscape:export-xdpi="65.472687"
inkscape:export-ydpi="65.472687">
<g
inkscape:export-ydpi="9.5134048"
inkscape:export-xdpi="9.5134048"
inkscape:export-filename="/Users/jarek/Projects/kopia-app/icons/converted/g886.png"
style="fill:#2a7fff;fill-opacity:1;stroke:none;stroke-width:0.57724;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
transform="matrix(19.66121,0,0,20.144513,53.912845,-5697.4527)"
id="g886">
<path
style="fill:#2a7fff;fill-opacity:1;stroke:none;stroke-width:0.365129;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 3.68292,285.81098 a 3.7143042,3.9255907 0 0 1 3.2394206,2.00885 2.978621,3.1480586 0 0 1 0.9055101,-0.15021 2.978621,3.1480586 0 0 1 2.9786373,3.14795 2.978621,3.1480586 0 0 1 -2.6197318,3.12262 v 0.0254 H 7.8278507 0.29155891 v -8.5e-4 a 2.3864853,2.5222394 0 0 1 -0.31315577,0.0246 2.3864853,2.5222394 0 0 1 -2.38640124,-2.52228 2.3864853,2.5222394 0 0 1 2.38640124,-2.52228 2.3864853,2.5222394 0 0 1 0.07019771,0.002 3.7143042,3.9255907 0 0 1 3.63432355,-3.136 z"
id="rect4799-3"
inkscape:connector-curvature="0"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
</g>
<g
inkscape:export-filename="/Users/jarek/Projects/kopia-app/icons/converted/g886.png"
inkscape:export-ydpi="9.5134048"
inkscape:export-xdpi="9.5134048"
style="fill:#e4e4e4;fill-opacity:1;stroke:none;stroke-width:0.280131;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g10"
transform="matrix(2.1802385,2.8772027,-1.7541947,3.575994,104.54534,-155.29438)">
<g
style="fill:#e4e4e4;fill-opacity:1;stroke:none;stroke-width:0.280131;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g883">
<path
inkscape:connector-curvature="0"
d="m 52.475497,57.904193 2.118969,-4.700554 c 0.258337,-0.573742 0.794452,-1.452559 1.188918,-1.952342 l 2.189741,-5.099694 c 0.171725,-0.02042 0.344927,-0.09102 0.466732,-0.194079 l 0.630265,-0.546749 c 0.243632,-0.206882 0.608139,-0.67108 0.736916,-0.95794 l 0.31965,-0.665782 c 0.05776,-0.130508 0.08615,-0.298735 0.08241,-0.463714 l 4.176599,-5.28537 c 0.206096,0.05958 0.452753,0.01539 0.625077,-0.131566 l 0.268535,-0.229977 c 0.242872,-0.207632 0.555873,-0.62096 0.686215,-0.907865 l 0.122894,-0.272803 c 0.08361,-0.186032 0.07645,-0.420341 0.0074,-0.621993 L 81.942439,16.980848 c 0.394443,-0.499011 -3.499736,2.827825 -4.003591,3.221242 L 63.158733,32.961034 c 0,0 -0.470077,-0.09048 -0.672002,2.42e-4 l -0.315191,0.142618 c -0.289847,0.131081 -0.692958,0.446742 -0.889398,0.696224 l -0.197198,0.248733 c -0.137168,0.173703 -0.173217,0.41621 -0.114352,0.619702 l -5.260067,4.10647 c -0.211006,-0.0255 -0.444855,0.0083 -0.614306,0.0842 l -0.765391,0.385589 c -0.289846,0.131081 -0.709835,0.462661 -0.906298,0.712915 l -0.479332,0.595593 c -0.106358,0.135778 -0.173913,0.333686 -0.180977,0.520579 l -4.899926,2.25652 c -0.503833,0.392646 -1.390045,0.927547 -1.970452,1.187416 l -5.715688,2.569121 c -0.579625,0.259846 -0.824906,1.000611 -0.618168,1.569324 0,0 0.927303,2.582054 3.611972,5.535017 l -2.71371,2.679373 c -0.644457,-0.484249 -0.865773,-0.265673 -1.130524,-0.0049 l -0.05151,0.05087 c -0.264752,0.260725 -0.48609,0.480073 0.0055,1.114567 l -3.777657,3.730903 c -0.643676,-0.484272 -0.865751,-0.266445 -1.129765,-0.0042 l -0.05148,0.0501 c -0.264751,0.260725 -0.485307,0.480051 0.0055,1.11534 L 22.80456,74.18879 c -0.113497,0.112842 1.950106,1.40756 1.950106,1.40756 l 11.27516,-10.990626 c 0.645262,0.483455 0.865773,0.265673 1.129742,0.005 l 0.05148,-0.0501 c 0.264774,-0.261497 0.486089,-0.480073 -0.0054,-1.116112 l 3.777612,-3.729358 c 0.643676,0.484272 0.865796,0.264901 1.129765,0.0042 l 0.05148,-0.0501 c 0.264797,-0.262269 0.48609,-0.480074 -0.0055,-1.11534 l 2.723413,-2.690456 c 3.01624,2.625495 6.011328,2.658358 6.011328,2.658358 0.577766,0.201527 1.322586,-0.04231 1.581751,-0.617623 z"
id="path8"
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
style="fill:#e4e4e4;fill-opacity:1;stroke:none;stroke-width:0.386847;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
<ellipse
inkscape:export-ydpi="9.5134048"
inkscape:export-xdpi="9.5134048"
inkscape:export-filename="/Users/jarek/Projects/kopia-app/icons/converted/g886.png"
ry="1.5360007"
rx="1.6261826"
cy="192.16341"
cx="190.19328"
id="path876"
style="fill:#b3b3b3;fill-opacity:1;stroke:#00ff00;stroke-width:0.179274;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

66
htmlui/src/uiutil.js Normal file
View File

@@ -0,0 +1,66 @@
export function sourceDisplayName(s) {
if (!s.host && !s.userName) {
return "(all)"
}
if (!s.userName) {
return "(all)@" + s.host;
}
return s.userName + "@" + s.host + ":" + s.path;
}
const base10UnitPrefixes = ["", "K", "M", "G", "T"];
function niceNumber(f) {
return (Math.round(f * 10) / 10.0) + '';
}
function toDecimalUnitString(f, thousand, prefixes, suffix) {
for (var i = 0; i < prefixes.length; i++) {
if (f < 0.9*thousand) {
return niceNumber(f) + ' ' + prefixes[i] + suffix;
}
f /= thousand
}
return niceNumber(f) + ' ' + prefixes[prefixes.length - 1] + suffix;
}
export function sizeDisplayName(s) {
return toDecimalUnitString(s, 1000, base10UnitPrefixes, "B");
}
export function intervalDisplayName(v) {
return "-";
}
export function timesOfDayDisplayName(v) {
if (!v) {
return "(none)";
}
return v.length + " times";
}
export function parseQuery(queryString) {
var query = {};
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
export function rfc3339TimestampDisplayName(n) {
let t = new Date(n);
return t.toLocaleString();
}
export function objectLink(n) {
if (n.startsWith("k")) {
return "/snapshots/dir/" + n;
}
return "/snapshots/file/" + n;
}

View File

@@ -4,7 +4,6 @@
"context"
"net/http"
"net/url"
"strings"
"time"
"github.com/kopia/kopia/fs"
@@ -70,7 +69,7 @@ func sourceMatchesURLFilter(src snapshot.SourceInfo, query url.Values) bool {
if v := query.Get("userName"); v != "" && src.UserName != v {
return false
}
if v := query.Get("path"); v != "" && !strings.Contains(src.Path, v) {
if v := query.Get("path"); v != "" && src.Path != v {
return false
}

View File

@@ -0,0 +1,334 @@
// Code generated for package server by go-bindata DO NOT EDIT. (@generated)
// sources:
// index.html
// +build !embedhtml
package server
import (
"bytes"
"compress/gzip"
"fmt"
"net/http"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
// Name return file name
func (fi bindataFileInfo) Name() string {
return fi.name
}
// Size return file size
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
// Mode return file mode
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
// Mode return file modify time
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
// IsDir return file whether a directory
func (fi bindataFileInfo) IsDir() bool {
return fi.mode&os.ModeDir != 0
}
// Sys return file is sys mode
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
type assetFile struct {
*bytes.Reader
name string
childInfos []os.FileInfo
childInfoOffset int
}
type assetOperator struct{}
// Open implement http.FileSystem interface
func (f *assetOperator) Open(name string) (http.File, error) {
var err error
if len(name) > 0 && name[0] == '/' {
name = name[1:]
}
content, err := Asset(name)
if err == nil {
return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil
}
children, err := AssetDir(name)
if err == nil {
childInfos := make([]os.FileInfo, 0, len(children))
for _, child := range children {
childPath := filepath.Join(name, child)
info, errInfo := AssetInfo(filepath.Join(name, child))
if errInfo == nil {
childInfos = append(childInfos, info)
} else {
childInfos = append(childInfos, newDirFileInfo(childPath))
}
}
return &assetFile{name: name, childInfos: childInfos}, nil
} else {
// If the error is not found, return an error that will
// result in a 404 error. Otherwise the server returns
// a 500 error for files not found.
if strings.Contains(err.Error(), "not found") {
return nil, os.ErrNotExist
}
return nil, err
}
}
// Close no need do anything
func (f *assetFile) Close() error {
return nil
}
// Readdir read dir's children file info
func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) {
if len(f.childInfos) == 0 {
return nil, os.ErrNotExist
}
if count <= 0 {
return f.childInfos, nil
}
if f.childInfoOffset+count > len(f.childInfos) {
count = len(f.childInfos) - f.childInfoOffset
}
offset := f.childInfoOffset
f.childInfoOffset += count
return f.childInfos[offset : offset+count], nil
}
// Stat read file info from asset item
func (f *assetFile) Stat() (os.FileInfo, error) {
if len(f.childInfos) != 0 {
return newDirFileInfo(f.name), nil
}
return AssetInfo(f.name)
}
// newDirFileInfo return default dir file info
func newDirFileInfo(name string) os.FileInfo {
return &bindataFileInfo{
name: name,
size: 0,
mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir
modTime: time.Time{}}
}
// AssetFile return a http.FileSystem instance that data backend by asset
func AssetFile() http.FileSystem {
return &assetOperator{}
}
var _indexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\x90\x3b\x4f\xfc\x30\x10\xc4\xfb\xff\xa7\x98\xff\x09\x5d\x45\xe2\xfe\x64\xd2\x00\x05\xa2\x80\x02\x84\x28\x37\xf1\x26\xb6\xce\x2f\xd9\xeb\x82\x6f\x8f\x92\x9c\xc4\x43\xa2\xf1\x43\x3b\xbf\x19\xed\xe8\xff\x77\x4f\xb7\x2f\xef\xcf\xf7\xb0\x12\xfc\xf0\x4f\xef\x17\x00\x68\xcb\x64\xf6\xe7\xf6\x15\x27\x9e\x87\xc7\x94\x1d\xe1\xf5\x01\x31\x09\x6a\xcb\x39\x15\x61\xd3\x6b\xb5\x8f\x77\x52\x7d\xa1\x7a\x4c\xe6\xe3\x9b\x4b\xfe\xc3\xe1\x1a\xad\x32\x28\x42\x13\x6c\xe1\xf9\xe6\x60\x45\x72\x3d\x29\xb5\x38\xb1\x6d\xec\xa7\x14\xd4\x79\x45\x2f\x67\x61\xcf\x54\xb9\x1e\x86\x34\xcf\x6e\x72\xe4\x31\x36\xe7\x8d\x56\x34\xf4\x5a\xe5\x1f\x99\x6f\x96\x23\xa6\x14\xb2\xf3\x2e\x2e\x98\x4b\x0a\xa8\xa9\x95\x89\xd7\xd8\xd3\x2f\x79\xe1\xe1\x0a\x81\xce\xbc\x95\xd2\x35\xd7\x8d\x2e\x1a\x12\xc2\xf1\x88\x25\xc1\xc5\x2a\xe4\x3d\x3a\xa1\xa5\x82\xc3\xc8\x66\x15\x6a\xb5\x92\x97\x06\xf6\xb5\xb5\xda\xfa\xfc\x0c\x00\x00\xff\xff\xe9\x99\x45\x19\x66\x01\x00\x00")
func indexHtmlBytes() ([]byte, error) {
return bindataRead(
_indexHtml,
"index.html",
)
}
func indexHtml() (*asset, error) {
bytes, err := indexHtmlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "index.html", size: 358, mode: os.FileMode(420), modTime: time.Unix(1574253768, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"index.html": indexHtml,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"index.html": &bintree{indexHtml, map[string]*bintree{}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Kopia UI not supported.</title>
</head>
<body>
<p>Kopia UI not supported, use an <a href="https://github.com/kopia/kopia/releases">official build</a>.</p>
<p>When compiling from source use:</p>
<pre>$ make html-ui-bindata && go install -tags embedhtml</pre>
</body>
</html>

View File

@@ -1,44 +1,23 @@
HUGO_VERSION=0.59.1
# don't put tools under current directory, otherwise 'make server' fails because there are too
# many open files due to 'node_modules'
TOOLS_DIR=$(CURDIR)/../.tools
NPM_TOOL=$(TOOLS_DIR)/nodejs/node/bin/npm
WATCH=true
TOOL_PATH=$(PATH):$(TOOLS_DIR)/nodejs/node/bin
HUGO_TOOL=$(TOOLS_DIR)/hugo/hugo
all: build
uname := $(shell uname -s)
include ../tools/tools.mk
build: gen-cli-reference-pages $(HUGO_TOOL) module-deps
build: gen-cli-reference-pages $(HUGO_TOOL) node_modules
PATH=$(TOOL_PATH) $(HUGO_TOOL)
server: $(HUGO_TOOL)
PATH=$(TOOL_PATH) $(HUGO_TOOL) server --watch=$(WATCH)
module-deps: node_modules
node_modules: $(NPM_TOOL)
PATH=$(TOOL_PATH) $(NPM_TOOL) install
$(HUGO_TOOL):
make install-tools
$(NPM_TOOL):
make -C .. install-webtools
clean:
rm -rf public/ resources/ node_modules/ $(TOOLS_DIR)/
install-tools:
mkdir -p $(TOOLS_DIR)/hugo
ifeq ($(uname),Linux)
curl -LsS https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_Linux-64bit.tar.gz | tar zxv -C $(TOOLS_DIR)/hugo
else
curl -LsS https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_macOS-64bit.tar.gz | tar zxv -C $(TOOLS_DIR)/hugo
endif
gen-cli-reference-pages:
rm -rf content/docs/Reference/Command-Line/Advanced
rm -rf content/docs/Reference/Command-Line/Common

55
tools/tools.mk Normal file
View File

@@ -0,0 +1,55 @@
SELF_DIR := $(realpath $(dir $(lastword $(MAKEFILE_LIST))))
TOOLS_DIR:=$(SELF_DIR)/.tools
uname := $(shell uname -s)
# tool versions
GOLANGCI_LINT_VERSION=v1.18.0
NODE_VERSION=12.13.0
HUGO_VERSION=0.59.1
# goveralls
GOVERALLS_TOOL=$(TOOLS_DIR)/bin/goveralls
$(GOVERALLS_TOOL):
mkdir -p $(TOOLS_DIR)
GO111MODULE=off GOPATH=$(TOOLS_DIR) go get github.com/mattn/goveralls
# nodejs / npm
NPM_TOOL=$(TOOLS_DIR)/nodejs/node/bin/npm
TOOL_PATH=$(PATH):$(TOOLS_DIR)/nodejs/node/bin
$(NPM_TOOL):
echo SELF_DIR: $(SELF_DIR)
mkdir -p $(TOOLS_DIR)/nodejs
ifeq ($(uname),Linux)
curl -LsS https://nodejs.org/dist/v$(NODE_VERSION)/node-v$(NODE_VERSION)-linux-x64.tar.gz | tar zx -C $(TOOLS_DIR)/nodejs
else
curl -LsS https://nodejs.org/dist/v$(NODE_VERSION)/node-v$(NODE_VERSION)-darwin-x64.tar.gz | tar zx -C $(TOOLS_DIR)/nodejs
endif
mv $(TOOLS_DIR)/nodejs/node-v$(NODE_VERSION)* $(TOOLS_DIR)/nodejs/node/
BINDATA_TOOL=$(TOOLS_DIR)/go-bindata
$(BINDATA_TOOL):
go build -o $(BINDATA_TOOL) github.com/go-bindata/go-bindata/go-bindata
# linter
LINTER_TOOL=$(TOOLS_DIR)/bin/golangci-lint
$(LINTER_TOOL):
mkdir -p $(TOOLS_DIR)
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(TOOLS_DIR)/bin/ $(GOLANGCI_LINT_VERSION)
# hugo
HUGO_TOOL=$(TOOLS_DIR)/hugo/hugo
$(HUGO_TOOL):
mkdir -p $(TOOLS_DIR)/hugo
ifeq ($(uname),Linux)
curl -LsS https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_Linux-64bit.tar.gz | tar zxv -C $(TOOLS_DIR)/hugo
else
curl -LsS https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_macOS-64bit.tar.gz | tar zxv -C $(TOOLS_DIR)/hugo
endif