mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-30 01:26:43 +08:00
feature (admin): admin console
This commit is contained in:
@ -83,6 +83,7 @@ a {
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
@ -100,8 +101,7 @@ button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
input, textarea, select {
|
||||
transition: border 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
@ -109,9 +109,9 @@ textarea {
|
||||
input[type="checkbox"] {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
|
||||
@ -14,13 +14,16 @@ export class ModalAlert extends Popup {
|
||||
alert.subscribe((Component, okCallback) => {
|
||||
this.setState({
|
||||
appear: true,
|
||||
value: Component
|
||||
value: Component,
|
||||
fn: okCallback
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(e){
|
||||
this.setState({appear: false});
|
||||
this.setState({appear: false}, () => {
|
||||
requestAnimationFrame(() => this.state.fn())
|
||||
});
|
||||
}
|
||||
|
||||
modalContentBody(){
|
||||
|
||||
@ -23,6 +23,8 @@ button{
|
||||
background: var(--emphasis);
|
||||
color: white
|
||||
}
|
||||
&.transparent{
|
||||
&.dark{
|
||||
background: var(--dark);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './container.scss';
|
||||
|
||||
export class Container extends React.Component {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { browserHistory } from 'react-router'
|
||||
import { browserHistory, Redirect } from 'react-router';
|
||||
|
||||
import { Session } from '../model/';
|
||||
import { Session, Admin } from '../model/';
|
||||
import { Container, Loader, Icon } from '../components/';
|
||||
import { memory, currentShare } from '../helpers/';
|
||||
|
||||
@ -47,7 +47,6 @@ export function LoggedInOnly(WrappedComponent){
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function ErrorPage(WrappedComponent){
|
||||
return class extends React.Component {
|
||||
constructor(props){
|
||||
@ -89,3 +88,11 @@ export function ErrorPage(WrappedComponent){
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const LoadingPage = (props) => {
|
||||
return (
|
||||
<div style={{marginTop: parseInt(window.innerHeight / 3)+'px'}}>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
170
client/components/formbuilder.js
Normal file
170
client/components/formbuilder.js
Normal file
@ -0,0 +1,170 @@
|
||||
import React from 'react';
|
||||
import { Input, Select, Enabler } from './';
|
||||
import { FormObjToJSON, bcrypt_password, format } from '../helpers/';
|
||||
|
||||
import "./formbuilder.scss";
|
||||
|
||||
export class FormBuilder extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
section(struct, key, level = 0){
|
||||
if(struct == null) struct = "";
|
||||
const isALeaf = function(struct){
|
||||
if("label" in struct && "type" in struct &&
|
||||
"value" in struct && "default" in struct){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if(Array.isArray(struct)) return null;
|
||||
else if(isALeaf(struct) === false){
|
||||
if(level <= 1){
|
||||
return (
|
||||
<div className="formbuilder">
|
||||
{
|
||||
key ? <h2 className="no-select">{ format(key) }</h2> : ""
|
||||
}
|
||||
{
|
||||
Object.keys(struct).map((key, index) => {
|
||||
return (
|
||||
<div key={key+"-"+index}>
|
||||
{ this.section(struct[key], key, level + 1) }
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend className="no-select">{ format(key) }</legend>
|
||||
{
|
||||
Object.keys(struct).map((key, index) => {
|
||||
return (
|
||||
<div key={key+"-"+index}>
|
||||
{ this.section(struct[key], key, level + 1) }
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let id = {};
|
||||
let target = [];
|
||||
if(struct.id !== undefined){
|
||||
id.id = this.props.idx === undefined ? struct.id : struct.id + "_" + this.props.idx;
|
||||
}
|
||||
if(struct.type === "enable"){
|
||||
target = struct.target.map((target) => {
|
||||
return this.props.idx === undefined ? target : target + "_" + this.props.idx;
|
||||
});
|
||||
}
|
||||
|
||||
const onChange = function(e, fn){
|
||||
struct.value = e;
|
||||
if(typeof fn === "function"){
|
||||
fn(struct);
|
||||
}
|
||||
this.props.onChange.call(
|
||||
this,
|
||||
FormObjToJSON(this.props.form)
|
||||
);
|
||||
};
|
||||
return ( <FormElement render={this.props.render} onChange={onChange.bind(this)} {...id} params={struct} target={target} name={ format(struct.label) } /> );
|
||||
}
|
||||
|
||||
render(){
|
||||
return this.section(this.props.form || {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const FormElement = (props) => {
|
||||
const id = props.id !== undefined ? {id: props.id} : {};
|
||||
let struct = props.params;
|
||||
let $input = ( <Input onChange={(e) => props.onChange(e.target.value)} {...id} name={props.name} type="text" defaultValue={struct.value} placeholder={struct.placeholder} /> );
|
||||
switch(props.params["type"]){
|
||||
case "text":
|
||||
const onTextChange = (value) => {
|
||||
if(value === ""){
|
||||
value = null;
|
||||
}
|
||||
props.onChange(value);
|
||||
};
|
||||
$input = ( <Input onChange={(e) => onTextChange(e.target.value)} {...id} name={props.name} type="text" value={struct.value || ""} placeholder={struct.placeholder}/> );
|
||||
break;
|
||||
case "number":
|
||||
const onNumberChange = (value) => {
|
||||
value = value === "" ? null : parseInt(value);
|
||||
props.onChange(value);
|
||||
};
|
||||
$input = ( <Input onChange={(e) => onNumberChange(e.target.value)} {...id} name={props.name} type="number" value={struct.value || ""} placeholder={struct.placeholder} /> );
|
||||
break;
|
||||
case "password":
|
||||
const onPasswordChange = (value) => {
|
||||
if(value === ""){
|
||||
value = null;
|
||||
}
|
||||
props.onChange(value);
|
||||
};
|
||||
$input = ( <Input onChange={(e) => onPasswordChange(e.target.value)} {...id} name={props.name} type="password" value={struct.value || ""} placeholder={struct.placeholder} /> );
|
||||
break;
|
||||
case "bcrypt":
|
||||
const onBcryptChange = (value) => {
|
||||
if(value === ""){
|
||||
return props.onChange(null);
|
||||
}
|
||||
bcrypt_password(value).then((hash) => {
|
||||
props.onChange(hash);
|
||||
});
|
||||
};
|
||||
$input = ( <Input onChange={(e) => onBcryptChange(e.target.value)} {...id} name={props.name} type="password" value={struct.value || ""} placeholder={struct.placeholder} /> );
|
||||
break;
|
||||
case "hidden":
|
||||
$input = ( <Input name={props.name} type="hidden" defaultValue={struct.value} /> );
|
||||
break;
|
||||
case "boolean":
|
||||
$input = ( <Input onChange={(e) => props.onChange(e.target.checked)} {...id} name={props.name} type="checkbox" checked={struct.value === null ? !!struct.default : struct.value} /> );
|
||||
break;
|
||||
case "select":
|
||||
$input = ( <Select onChange={(e) => props.onChange(e.target.value)} {...id} name={props.name} choices={struct.options} value={struct.value === null ? struct.default : struct.value} placeholder={struct.placeholder} />);
|
||||
break;
|
||||
case "enable":
|
||||
$input = ( <Enabler onChange={(e) => props.onChange(e.target.checked)} {...id} name={props.name} target={props.target} defaultValue={struct.value === null ? struct.default : struct.value} /> );
|
||||
break;
|
||||
case "image":
|
||||
$input = ( <img {...id} src={props.value} /> );
|
||||
break;
|
||||
}
|
||||
|
||||
if(props.render){
|
||||
return props.render($input, props, struct, props.onChange.bind(null, null));
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={"no-select input_type_" + props.params["type"]}>
|
||||
<div>
|
||||
<span>
|
||||
{ format(struct.label) }:
|
||||
</span>
|
||||
<div style={{width: '100%'}}>
|
||||
{ $input }
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="nothing"></span>
|
||||
<div style={{width: '100%'}}>
|
||||
{ struct.description ? (<div className="description">{struct.description}</div>) : null }
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
28
client/components/formbuilder.scss
Normal file
28
client/components/formbuilder.scss
Normal file
@ -0,0 +1,28 @@
|
||||
.formbuilder{
|
||||
input[type="checkbox"]{
|
||||
top: 5px;
|
||||
}
|
||||
.description{
|
||||
margin-top: -5px;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.25;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
input::placeholder{
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
label.input_type_hidden{
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset{
|
||||
legend{
|
||||
text-transform: uppercase;
|
||||
font-weight: 200;
|
||||
font-size: 1em;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
export { EventEmitter, EventReceiver } from './events';
|
||||
export { BreadCrumb, PathElement } from './breadcrumb';
|
||||
export { Input } from './input';
|
||||
export { Input, Select, Enabler } from './input';
|
||||
export { Textarea } from './textarea';
|
||||
export { Button } from './button';
|
||||
export { Container } from './container';
|
||||
@ -19,7 +19,5 @@ export { Audio } from './audio';
|
||||
export { Video } from './video';
|
||||
export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown';
|
||||
export { MapShot } from './mapshot';
|
||||
export { LoggedInOnly, ErrorPage } from './decorator';
|
||||
//export { Connect } from './connect';
|
||||
// Those are commented because they will delivered as a separate chunk
|
||||
// export { Editor } from './editor';
|
||||
export { LoggedInOnly, ErrorPage, LoadingPage } from './decorator';
|
||||
export { FormBuilder } from './formbuilder';
|
||||
|
||||
@ -11,9 +11,9 @@ export class Input extends React.Component {
|
||||
return (
|
||||
<input
|
||||
className="component_input"
|
||||
onChange={this.props.onChange}
|
||||
{...this.props}
|
||||
ref={(comp) => { this.ref = comp; }}
|
||||
/>
|
||||
ref={(comp) => { this.ref = comp; }} />
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -22,3 +22,67 @@ Input.propTypes = {
|
||||
type: PropTypes.string,
|
||||
placeholder: PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
export const Select = (props) => {
|
||||
const choices = props.choices || [];
|
||||
const id = props.id ? {id: props.id} : {};
|
||||
return (
|
||||
<select className="component_select" {...id} name={props.name} onChange={props.onChange} defaultValue={props.value}>
|
||||
<option hidden>{props.placeholder}</option>
|
||||
{
|
||||
choices.map((choice, index) => {
|
||||
return (
|
||||
<option key={index} name={choice}>{choice}</option>
|
||||
);
|
||||
})
|
||||
}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export class Enabler extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
requestAnimationFrame(() => {
|
||||
this.toggle(this.props.defaultValue || false);
|
||||
});
|
||||
}
|
||||
|
||||
onChange(e){
|
||||
this.toggle(e.target.checked);
|
||||
this.props.onChange(e);
|
||||
}
|
||||
|
||||
toggle(value){
|
||||
const target = this.props.target || [];
|
||||
target.map((t) => {
|
||||
let $el = document.getElementById(t);
|
||||
if(!$el) return;
|
||||
if(value === true){
|
||||
$el.parentElement.parentElement.parentElement.style.display = "block";
|
||||
$el.parentElement.parentElement.parentElement.style.opacity = "1";
|
||||
} else {
|
||||
$el.parentElement.parentElement.parentElement.style.display = "none";
|
||||
$el.parentElement.parentElement.parentElement.style.opacity = "0";
|
||||
|
||||
// reset value
|
||||
if($el.value){
|
||||
$el.value = null;
|
||||
let event = new Event('input', { bubbles: true});
|
||||
event.simulated = true;
|
||||
$el.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<Input type="checkbox" onChange={this.onChange.bind(this)} defaultChecked={this.props.defaultValue} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -21,3 +21,13 @@
|
||||
border-color: var(--emphasis-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.component_select{
|
||||
background: inherit;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ export function http_get(url, type = 'json'){
|
||||
if(type === 'json'){
|
||||
try{
|
||||
let data = JSON.parse(xhr.responseText);
|
||||
if(data.status === 'ok'){
|
||||
if("status" in data === false || data.status === 'ok'){
|
||||
done(data);
|
||||
}else{
|
||||
err(data);
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const algorithm = 'aes-256-cbc';
|
||||
|
||||
export function encrypt(obj, key){
|
||||
@ -11,3 +13,12 @@ export function decrypt(text, key){
|
||||
var decipher = crypto.createDecipher(algorithm, key)
|
||||
return JSON.parse(decipher.update(text,'base64','utf8') + decipher.final('utf8'));
|
||||
}
|
||||
|
||||
export function bcrypt_password(password) {
|
||||
return new Promise((done, error) => {
|
||||
bcrypt.hash(password, 10, (err, hash) => {
|
||||
if(err) return error(err)
|
||||
done(hash);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
16
client/helpers/form.js
Normal file
16
client/helpers/form.js
Normal file
@ -0,0 +1,16 @@
|
||||
export const FormObjToJSON = function(o, fn){
|
||||
let obj = Object.assign({}, o);
|
||||
Object.keys(obj).map((key) => {
|
||||
let t = obj[key];
|
||||
if("label" in t && "type" in t && "default" in t && "value" in t){
|
||||
if(typeof fn === "function"){
|
||||
fn(obj, key);
|
||||
} else {
|
||||
obj[key] = obj[key].value;
|
||||
}
|
||||
} else {
|
||||
obj[key] = FormObjToJSON(obj[key], fn);
|
||||
}
|
||||
});
|
||||
return obj
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate';
|
||||
export { opener } from './mimetype';
|
||||
export { debounce, throttle } from './backpressure';
|
||||
export { encrypt, decrypt } from './crypto';
|
||||
export { encrypt, decrypt, bcrypt_password } from './crypto';
|
||||
export { event } from './events';
|
||||
export { cache } from './cache';
|
||||
export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, appendShareToUrl } from './path';
|
||||
@ -14,3 +14,5 @@ export { gid, randomString } from './random';
|
||||
export { leftPad, copyToClipboard } from './common';
|
||||
export { getMimeType } from './mimetype';
|
||||
export { settings_get, settings_put } from './settings';
|
||||
export { FormObjToJSON } from './form';
|
||||
export { format } from './text';
|
||||
|
||||
10
client/helpers/text.js
Normal file
10
client/helpers/text.js
Normal file
@ -0,0 +1,10 @@
|
||||
export function format(str = ""){
|
||||
if(str.length === 0) return str;
|
||||
return str.split("_")
|
||||
.map((word, index) => {
|
||||
|
||||
if(index != 0) return word;
|
||||
return word[0].toUpperCase() + word.substring(1);
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
@ -26,7 +26,6 @@
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/assets/logo/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/logo/favicon-16x16.png">
|
||||
<link rel="icon" href="/assets/logo/favicon.ico" type="image/x-icon" />
|
||||
<script src="/api/config"></script>
|
||||
|
||||
<meta name="msapplication-TileColor" content="#f2f2f2">
|
||||
<meta name="msapplication-TileImage" content="/assets/logo/ms-icon-144x144.png">
|
||||
@ -35,6 +34,19 @@
|
||||
<meta name="description" content="browse your files in the cloud">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main" style="height: 100%"></div>
|
||||
<div id="main" style="height: 100%">
|
||||
<style>
|
||||
html{
|
||||
background: #f2f3f5;
|
||||
color: #375160;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
if(location.pathname == "/" || location.pathname == "/login"){
|
||||
$style = document.querySelector("style");
|
||||
$style.innerText = $style.innerText.replace("#f2f3f5", "#9AD1ED")
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -2,13 +2,17 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Router from './router';
|
||||
|
||||
import { Config } from "./model/"
|
||||
|
||||
import './assets/css/reset.scss';
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const className = 'ontouchstart' in window ? 'touch-yes' : 'touch-no';
|
||||
document.body.classList.add(className);
|
||||
|
||||
ReactDOM.render(<Router/>, document.getElementById('main'));
|
||||
Config.refresh().then(() => {
|
||||
ReactDOM.render(<Router/>, document.getElementById('main'));
|
||||
});
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/assets/worker/cache.js').catch(function(error) {
|
||||
|
||||
10
client/model/admin.js
Normal file
10
client/model/admin.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { http_post, http_get } from '../helpers';
|
||||
|
||||
export const Admin = {
|
||||
login: function(password = ""){
|
||||
return http_post("/admin/api/session", {password: password});
|
||||
},
|
||||
isAdmin: function(){
|
||||
return http_get("/admin/api/session").then((res) => res.result);
|
||||
}
|
||||
};
|
||||
61
client/model/config.js
Normal file
61
client/model/config.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { http_get, http_post, http_delete, debounce } from '../helpers/';
|
||||
|
||||
class ConfigModel {
|
||||
constructor(){}
|
||||
|
||||
all(){
|
||||
return http_get("/admin/api/config").then((d) => d.result);
|
||||
}
|
||||
|
||||
save(config, debounced = true, fn_ok, fn_err){
|
||||
let url = "/admin/api/config";
|
||||
|
||||
if(debounced){
|
||||
if(!this.debounced_post){
|
||||
this.debounced_post = debounce((url, config) => {
|
||||
return http_post(url, config).then(this.refresh).then((a) => {
|
||||
if(typeof fn_ok === "function") return fn_ok();
|
||||
return Promise.resolve(a)
|
||||
}).catch((err) => {
|
||||
if(typeof fn_err === "function") return fn_err();
|
||||
return Promise.reject(err)
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
return this.debounced_post(url, config)
|
||||
}
|
||||
return http_post(url, config).then(this.refresh).then((a) => {
|
||||
if(typeof fn_ok === "function") return fn_ok();
|
||||
return Promise.resolve(a)
|
||||
}).catch((err) => {
|
||||
if(typeof fn_err === "function") return fn_err();
|
||||
return Promise.reject(err)
|
||||
});
|
||||
}
|
||||
|
||||
refresh(){
|
||||
return http_get("/api/config").then((config) => {
|
||||
window.CONFIG = config.result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PluginModel {
|
||||
constructor(){}
|
||||
|
||||
all(){
|
||||
return http_get("/admin/api/plugin").then((r) => r.results);
|
||||
}
|
||||
}
|
||||
|
||||
class BackendModel {
|
||||
constructor(){}
|
||||
|
||||
all(){
|
||||
return http_get("/admin/api/backend").then((r) => r.result);
|
||||
}
|
||||
}
|
||||
|
||||
export const Plugin = new PluginModel();
|
||||
export const Config = new ConfigModel();
|
||||
export const Backend = new BackendModel();
|
||||
@ -1,3 +1,6 @@
|
||||
export { Files } from './files';
|
||||
export { Session } from './session';
|
||||
export { Share } from './share';
|
||||
export { Files } from "./files";
|
||||
export { Session } from "./session";
|
||||
export { Share } from "./share";
|
||||
export { Config, Plugin, Backend } from "./config";
|
||||
export { Log } from "./log";
|
||||
export { Admin } from "./admin"
|
||||
|
||||
19
client/model/log.js
Normal file
19
client/model/log.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { http_get } from '../helpers/';
|
||||
|
||||
class LogManager{
|
||||
constructor(){}
|
||||
|
||||
get(maxSize = -1){
|
||||
let url = this.url();
|
||||
if(maxSize > 0){
|
||||
url += "?maxSize="+maxSize
|
||||
}
|
||||
return http_get(url, 'text');
|
||||
}
|
||||
|
||||
url(){
|
||||
return "/admin/api/log"
|
||||
}
|
||||
}
|
||||
|
||||
export const Log = new LogManager();
|
||||
113
client/pages/adminpage.js
Normal file
113
client/pages/adminpage.js
Normal file
@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import Path from 'path';
|
||||
import { Route, Switch, Link, NavLink } from 'react-router-dom';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
|
||||
import './error.scss';
|
||||
import './adminpage.scss';
|
||||
import { Icon, LoadingPage } from '../components/';
|
||||
import { Config, Admin } from '../model';
|
||||
import { notify } from '../helpers/';
|
||||
import { HomePage, DashboardPage, ConfigPage, LogPage, PluginPage, SupportPage, SetupPage, LoginPage } from './adminpage/';
|
||||
|
||||
|
||||
function AdminOnly(WrappedComponent){
|
||||
return class extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdmin: null
|
||||
};
|
||||
this.admin = () => {
|
||||
Admin.isAdmin().then((t) => {
|
||||
this.setState({isAdmin: t});
|
||||
}).catch((err) => {
|
||||
notify.send("Error: " + (err && err.message) , "error");
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
this.timeout = window.setInterval(this.admin.bind(this), 30 * 1000);
|
||||
this.admin.call(this);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.clearInterval(this.timeout);
|
||||
}
|
||||
|
||||
render(){
|
||||
if(this.state.isAdmin === true){
|
||||
return ( <WrappedComponent {...this.props} /> );
|
||||
} else if(this.state.isAdmin === false) {
|
||||
return ( <LoginPage reload={() => this.admin()} /> );
|
||||
}
|
||||
return ( <LoadingPage />);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
export class AdminPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdmin: null
|
||||
};
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div className="component_page_admin">
|
||||
<SideMenu url={this.props.match.url}/>
|
||||
<div className="page_container scroll-y">
|
||||
<ReactCSSTransitionGroup key={window.location.pathname} transitionName="adminpage" transitionLeave={true} transitionEnter={true} transitionLeaveTimeout={15000} transitionEnterTimeout={20000} transitionAppear={true} transitionAppearTimeout={20000}>
|
||||
<Switch>
|
||||
<Route path={this.props.match.url + "/dashboard"} component={DashboardPage} />
|
||||
<Route path={this.props.match.url + "/configure"} component={ConfigPage} />
|
||||
<Route path={this.props.match.url + "/activity"} component={LogPage} />
|
||||
<Route path={this.props.match.url + "/plugins"} component={PluginPage} />
|
||||
<Route path={this.props.match.url + "/support"} component={SupportPage} />
|
||||
<Route path={this.props.match.url + "/setup"} component={SetupPage} />
|
||||
<Route path={this.props.match.url} component={HomePage} />
|
||||
</Switch>
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SideMenu = (props) => {
|
||||
return (
|
||||
<div className="component_menu_sidebar no-select">
|
||||
<NavLink to="/" className="header">
|
||||
<Icon name="arrow_left" />
|
||||
<img src="/assets/logo/icon-192x192.png" />
|
||||
</NavLink>
|
||||
<h2>Admin console</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<NavLink activeClassName="active" to={props.url + "/dashboard"}>
|
||||
Dashboard
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink activeClassName="active" to={props.url + "/configure"}>
|
||||
Configure
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink activeClassName="active" to={props.url + "/activity"}>
|
||||
Activity
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink activeClassName="active" to={props.url + "/support"}>
|
||||
Support
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
165
client/pages/adminpage.scss
Normal file
165
client/pages/adminpage.scss
Normal file
@ -0,0 +1,165 @@
|
||||
.component_page_admin{
|
||||
display: flex;
|
||||
.page_container{
|
||||
width: 100%;
|
||||
background: var(--super-light);
|
||||
padding-bottom: 150px;
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
@media screen and (max-width: 1000px) { padding-left: 30px; padding-right: 30px; }
|
||||
@media screen and (max-width: 500px) { padding-left: 10px; padding-right: 10px; }
|
||||
|
||||
box-sizing: border-box;
|
||||
max-height: 100vh;
|
||||
|
||||
h2{
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
text-shadow: 0 0 2px var(--bg-color);
|
||||
font-size: 2.8em;
|
||||
padding: 60px 0 0 0;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 0;
|
||||
@media screen and (max-width: 1000px) { padding: 25px 0 0 0; }
|
||||
@media screen and (max-width: 500px) { padding: 10px 0 0 0; }
|
||||
font-weight: 300;
|
||||
line-height: 1em;
|
||||
&:after{
|
||||
content: "_";
|
||||
display: block;
|
||||
font-size: 0;
|
||||
border-bottom: 3px solid var(--color);
|
||||
width: 90px;
|
||||
margin-top: 10px;
|
||||
opacity: 0.9;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
.sticky h2{
|
||||
position: sticky;
|
||||
background: var(--super-light);
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
}
|
||||
label{
|
||||
> div{
|
||||
display: flex;
|
||||
@media screen and (max-width: 550px) {
|
||||
display: block;
|
||||
.nothing{ display: none; }
|
||||
}
|
||||
> span{
|
||||
display: inline-block;
|
||||
line-height: 30px;
|
||||
min-width: 150px;
|
||||
@media screen and (max-width: 760px) { min-width: 115px }
|
||||
padding-right: 20px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
a{
|
||||
color: var(--dark);
|
||||
border-bottom: 1px dashed var(--dark);
|
||||
}
|
||||
pre{
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
background: var(--dark);
|
||||
padding: 10px;
|
||||
margin-bottom: 0;
|
||||
border-radius: 2px;
|
||||
color: white;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.component_loader > svg{
|
||||
height: 50px;
|
||||
}
|
||||
fieldset{
|
||||
background: white;
|
||||
border-color: var(--super-light);
|
||||
border-radius: 3px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.component_menu_sidebar{
|
||||
height: 100vh;
|
||||
background: var(--dark);
|
||||
width: 250px;
|
||||
border-right: 2px solid var(--color);
|
||||
padding: 50px 0px 0px 40px;
|
||||
transition: transform 0.3s ease;
|
||||
transition-delay: 0.7s;
|
||||
h2{
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
color: var(--primary);
|
||||
font-weight: 300;
|
||||
font-size: 1.5em;
|
||||
margin: 25px 0 40px 0;
|
||||
}
|
||||
ul {
|
||||
color: var(--light);
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
li {
|
||||
margin: 15px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
a.active, a:hover{
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.header{
|
||||
img{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
img[alt="arrow_left"]{
|
||||
position: absolute;
|
||||
margin-left: -35px;
|
||||
opacity: 0.7;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1000px) { padding-left: 30px; width: 200px; }
|
||||
@media screen and (max-width: 760px) {
|
||||
padding: 10px;
|
||||
h2{
|
||||
margin: 15px 0 25px 0;
|
||||
font-size: 1.25em;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 650px) {
|
||||
width: inherit;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
h2{ display: none; }
|
||||
}
|
||||
@media screen and (max-width: 440px) {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
ul li{
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.adminpage-appear{
|
||||
transition: transform 0.3s ease, opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.adminpage-appear.adminpage-appear-active{
|
||||
opacity: 1;
|
||||
transform: translateX(0px);
|
||||
}
|
||||
47
client/pages/adminpage/config.js
Normal file
47
client/pages/adminpage/config.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { FormBuilder } from '../../components/';
|
||||
import { Config } from '../../model/';
|
||||
|
||||
export class ConfigPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
form: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
Config.all().then((log) => {
|
||||
this.setState({form: log});
|
||||
});
|
||||
}
|
||||
|
||||
format(name){
|
||||
if(typeof name !== "string"){
|
||||
return "N/A";
|
||||
}
|
||||
return name
|
||||
.split("_")
|
||||
.map((word) => {
|
||||
if(word.length < 1){
|
||||
return word;
|
||||
}
|
||||
return word[0].toUpperCase() + word.substring(1);
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
onChange(form){
|
||||
form.connections = window.CONFIG.connections
|
||||
Config.save(form);
|
||||
this.setState({refresh: Math.random()});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<form className="sticky">
|
||||
<FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
197
client/pages/adminpage/dashboard.js
Normal file
197
client/pages/adminpage/dashboard.js
Normal file
@ -0,0 +1,197 @@
|
||||
import React from 'react';
|
||||
import { FormBuilder, Icon, Input } from "../../components/";
|
||||
import { Backend, Config } from "../../model/";
|
||||
import { FormObjToJSON, notify, format } from "../../helpers/";
|
||||
|
||||
import "./dashboard.scss";
|
||||
|
||||
export class DashboardPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
backend_enabled: [],
|
||||
backend_available: [],
|
||||
config: null
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
Promise.all([
|
||||
Backend.all(),
|
||||
Config.all()
|
||||
]).then((data) => {
|
||||
let [backend, config] = data;
|
||||
this.setState({
|
||||
backend_available: backend,
|
||||
backend_enabled: window.CONFIG.connections.map((conn) => {
|
||||
return createFormBackend(backend, conn);
|
||||
}),
|
||||
config: config
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onChange(e){
|
||||
this.setState({refresh: Math.random()}); // refresh the screen to refresh the mutation
|
||||
// that have happenned down the stack
|
||||
|
||||
let json = FormObjToJSON(this.state.config);
|
||||
json.connections = this.state.backend_enabled.map((backend) => {
|
||||
let data = FormObjToJSON(backend, (obj, key) => {
|
||||
if(obj[key].enabled === true){
|
||||
obj[key] = obj[key].value || obj[key].default;
|
||||
} else {
|
||||
delete obj[key];
|
||||
}
|
||||
});
|
||||
const key = Object.keys(data)[0];
|
||||
return data[key];
|
||||
});
|
||||
|
||||
// persist config object in the backend
|
||||
Config.save(json, true, () => {
|
||||
this.componentWillMount();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
addBackend(backend_id){
|
||||
this.setState({
|
||||
backend_enabled: this.state.backend_enabled.concat(
|
||||
createFormBackend(this.state.backend_available, {
|
||||
type: backend_id,
|
||||
label: backend_id.toUpperCase()
|
||||
})
|
||||
)
|
||||
}, this.onChange.bind(this));
|
||||
}
|
||||
|
||||
removeBackend(n){
|
||||
this.setState({
|
||||
backend_enabled: this.state.backend_enabled.filter((_, i) => i !== n)
|
||||
}, this.onChange.bind(this));
|
||||
}
|
||||
|
||||
render(){
|
||||
const update = (value, struct) => {
|
||||
struct.enabled = value;
|
||||
this.setState({refresh: Math.random()});
|
||||
if(value === false){
|
||||
struct.value = null;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const enable = (struct) => {
|
||||
if(typeof struct.value === "string"){
|
||||
struct.enabled = true;
|
||||
return true;
|
||||
}
|
||||
return !!struct.enabled;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="component_dashboard">
|
||||
<h2>Dashboard</h2>
|
||||
<div className="box-element">
|
||||
{
|
||||
Object.keys(this.state.backend_available).map((backend_available, index) => {
|
||||
return (
|
||||
<div key={index} className="backend">
|
||||
<div>
|
||||
{backend_available}
|
||||
<span className="no-select" onClick={this.addBackend.bind(this, backend_available)}>
|
||||
+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<form>
|
||||
{
|
||||
this.state.backend_enabled.map((backend_enable, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="icons no-select" onClick={this.removeBackend.bind(this, index)}>
|
||||
<Icon name="delete" />
|
||||
</div>
|
||||
<FormBuilder onChange={this.onChange.bind(this)} idx={index} key={index}
|
||||
form={{"": backend_enable}}
|
||||
render={ ($input, props, struct, onChange) => {
|
||||
let $checkbox = (
|
||||
<Input type="checkbox" style={{width: "inherit", marginRight: '6px', top: '6px'}}
|
||||
checked={enable(struct)} onChange={(e) => onChange(update.bind(this, e.target.checked))}/>
|
||||
);
|
||||
if(struct.label === "label"){
|
||||
$checkbox = null;
|
||||
}
|
||||
return (
|
||||
<label className={"no-select input_type_" + props.params["type"]}>
|
||||
<div>
|
||||
<span>
|
||||
{ $checkbox }
|
||||
{ format(struct.label) }:
|
||||
</span>
|
||||
<div style={{width: '100%'}}>
|
||||
{ $input }
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="nothing"></span>
|
||||
<div style={{width: '100%'}}>
|
||||
{
|
||||
struct.description ? (<div className="description">{struct.description}</div>) : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function createFormBackend(backend_available, backend_data){
|
||||
let template = JSON.parse(JSON.stringify(backend_available[backend_data.type]));
|
||||
|
||||
for(let key in backend_data){
|
||||
if(key in template){
|
||||
template[key].value = backend_data[key];
|
||||
template[key].enabled = true;
|
||||
} else {
|
||||
// create a form object if data isn't available in the template
|
||||
let obj = {};
|
||||
obj[key] = {
|
||||
label: key,
|
||||
type: "text",
|
||||
value: null,
|
||||
default: backend_data[key]
|
||||
};
|
||||
template = Object.assign(obj, template);
|
||||
}
|
||||
|
||||
if(key === "label"){
|
||||
template[key].placeholder = "Name as shown on the login screen.";
|
||||
template[key].value = backend_data[key];
|
||||
template[key].enabled = true;
|
||||
} else if(key === "type"){
|
||||
template[key].enabled = true;
|
||||
} else if(key === "advanced"){
|
||||
template[key].enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
let obj = {};
|
||||
obj[backend_data.type] = template;
|
||||
return obj;
|
||||
}
|
||||
63
client/pages/adminpage/dashboard.scss
Normal file
63
client/pages/adminpage/dashboard.scss
Normal file
@ -0,0 +1,63 @@
|
||||
.component_dashboard{
|
||||
.box-element {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -5px -5px 20px -5px;
|
||||
.backend{
|
||||
position: relative;
|
||||
width: 20%;
|
||||
@media (max-width: 1350px){width: 25%;}
|
||||
@media (max-width: 900px){width: 33.33%;}
|
||||
@media (max-width: 750px){width: 50%;}
|
||||
span{
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
font-size: 1.5em;
|
||||
font-family: monospace;
|
||||
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
line-height: 25px;
|
||||
|
||||
background: var(--bg-color);
|
||||
border-radius: 50%;
|
||||
box-shadow: 2px 2px 2px var(--light);
|
||||
color: var(--color);
|
||||
text-shadow: none;
|
||||
}
|
||||
> div {
|
||||
box-shadow: 2px 2px 10px var(--emphasis-primary);
|
||||
margin: 8px;
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
text-shadow: 0px 0px 1px var(--color);
|
||||
font-size: 1.1em;
|
||||
text-transform: uppercase;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form{
|
||||
> div{
|
||||
position: relative;
|
||||
> .icons{
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
padding: 10px;
|
||||
background: var(--light);
|
||||
cursor: pointer;
|
||||
right: -15px;
|
||||
top: -5px;
|
||||
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px;
|
||||
.component_icon{
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
client/pages/adminpage/home.js
Normal file
15
client/pages/adminpage/home.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
export class HomePage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
stage: "loading"
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
return ( <Redirect to="/admin/dashboard" /> );
|
||||
}
|
||||
}
|
||||
8
client/pages/adminpage/index.js
Normal file
8
client/pages/adminpage/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
export { LogPage } from "./logger";
|
||||
export { HomePage } from "./home";
|
||||
export { ConfigPage } from "./config";
|
||||
export { PluginPage } from "./plugin";
|
||||
export { SupportPage } from "./support";
|
||||
export { DashboardPage } from "./dashboard";
|
||||
export { SetupPage } from "./setup";
|
||||
export { LoginPage } from "./loginpage";
|
||||
70
client/pages/adminpage/logger.js
Normal file
70
client/pages/adminpage/logger.js
Normal file
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { FormBuilder, Loader, Button, Icon } from '../../components/';
|
||||
import { Config, Log } from '../../model/';
|
||||
import { FormObjToJSON, notify } from '../../helpers/';
|
||||
|
||||
import "./logger.scss";
|
||||
|
||||
export class LogPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
form: {},
|
||||
loading: false,
|
||||
log: "",
|
||||
config: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
Config.all().then((config) => {
|
||||
this.setState({
|
||||
form: {"":{"params":config["log"]}},
|
||||
config: FormObjToJSON(config)
|
||||
});
|
||||
});
|
||||
Log.get(1024*100).then((log) => { // get only the last 100kb of log
|
||||
this.setState({log: log}, () => {
|
||||
this.refs.$log.scrollTop = this.refs.$log.scrollHeight;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onChange(r){
|
||||
this.state.config["log"] = r[""].params;
|
||||
this.state.config["connections"] = window.CONFIG.connections;
|
||||
this.setState({loading: true}, () => {
|
||||
Config.save(this.state.config, false, () => {
|
||||
this.setState({loading: false});
|
||||
}, () => {
|
||||
notify.send("Error while saving config", "error");
|
||||
this.setState({loading: false});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render(){
|
||||
const filename = () => {
|
||||
let tmp = "access_";
|
||||
tmp += new Date().toISOString().substring(0,10).replace(/-/g, "");
|
||||
tmp += ".log";
|
||||
}
|
||||
return (
|
||||
<div className="component_logpage">
|
||||
<h2>Logging { this.state.loading === true ? <Icon style={{height: '40px'}} name="loading"/> : null}</h2>
|
||||
<div style={{minHeight: '150px'}}>
|
||||
<FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} />
|
||||
</div>
|
||||
|
||||
<pre style={{height: '350px'}} ref="$log">
|
||||
{
|
||||
this.state.log === "" ? <Loader/> : this.state.log + "\n\n\n\n\n"
|
||||
}
|
||||
</pre>
|
||||
<div>
|
||||
<a href={Log.url()} download={filename()}><Button className="primary">Download</Button></a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
9
client/pages/adminpage/logger.scss
Normal file
9
client/pages/adminpage/logger.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.component_logpage{
|
||||
button{
|
||||
width: inherit;
|
||||
float: right;
|
||||
margin-top: 5px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
53
client/pages/adminpage/loginpage.js
Normal file
53
client/pages/adminpage/loginpage.js
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
import { Input, Button, Container, Icon, Loader } from '../../components/';
|
||||
import { Config, Admin } from '../../model/';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
|
||||
export class LoginPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.refs.$input.ref.focus();
|
||||
}
|
||||
|
||||
authenticate(e){
|
||||
e.preventDefault();
|
||||
this.setState({loading: true});
|
||||
Admin.login(this.refs.$input.ref.value)
|
||||
.then(() => this.props.reload())
|
||||
.catch(() => {
|
||||
this.refs.$input.ref.value = "";
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: true
|
||||
}, () => {
|
||||
window.setTimeout(() => {
|
||||
this.setState({error: false});
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render(){
|
||||
const marginTop = () => { return {marginTop: parseInt(window.innerHeight / 3)+'px'};};
|
||||
|
||||
return (
|
||||
<Container maxWidth="300px" className="sharepage_component">
|
||||
<form className={this.state.error ? "error" : ""} onSubmit={this.authenticate.bind(this)} style={marginTop()}>
|
||||
<Input ref="$input" type="text" placeholder="Password" />
|
||||
<Button theme="transparent">
|
||||
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
41
client/pages/adminpage/plugin.js
Normal file
41
client/pages/adminpage/plugin.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Plugin } from '../../model/';
|
||||
|
||||
import './plugin.scss';
|
||||
|
||||
const PluginBox = (props) => {
|
||||
return (
|
||||
<div className="component_pluginbox">
|
||||
<div className="title">{props.name}</div>
|
||||
<div>{props.description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export class PluginPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
plugins: []
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
Plugin.all().then((list) => this.setState({plugins: list}));
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div className="component_plugin">
|
||||
<h2>Plugins</h2>
|
||||
<div>
|
||||
{
|
||||
this.state.plugins.map((plugin, index) => {
|
||||
return ( <PluginBox key={index} name={plugin.name} author={plugin.author} description={plugin.description} /> );
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
21
client/pages/adminpage/plugin.scss
Normal file
21
client/pages/adminpage/plugin.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.component_pluginbox{
|
||||
background: white;
|
||||
padding: 10px;
|
||||
box-shadow: 2px 2px 2px var(--bg-color);
|
||||
margin-bottom: 10px;
|
||||
.title{
|
||||
color: var(--dark);
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.component_plugin{
|
||||
input[type="file"]{
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border: 2px solid var(--bg-color);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
113
client/pages/adminpage/setup.js
Normal file
113
client/pages/adminpage/setup.js
Normal file
@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Input, Button, Container, Icon } from '../../components/';
|
||||
import { Config, Admin } from '../../model/';
|
||||
import { notify, FormObjToJSON, alert, prompt, bcrypt_password } from '../../helpers';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
|
||||
import "./setup.scss";
|
||||
|
||||
export class SetupPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
stage: 0,
|
||||
password: "",
|
||||
enable_telemetry: false,
|
||||
creating_password: false
|
||||
};
|
||||
}
|
||||
|
||||
createPassword(e){
|
||||
this.setState({creating_password: true});
|
||||
e.preventDefault();
|
||||
Config.all().then((config) => {
|
||||
this.setState({enable_telemetry: config.log.telemetry.value}, () => {
|
||||
if(this.state.enable_telemetry === true) return;
|
||||
this.unlisten = this.props.history.listen((location, action) => {
|
||||
alert.now((
|
||||
<div>
|
||||
<p style={{textAlign: 'justify'}}>
|
||||
Help making this software better by sending crash reports and anonymous usage statistics
|
||||
</p>
|
||||
<form onSubmit={this.start.bind(this)} style={{fontSize: '0.9em', marginTop: '10px'}}>
|
||||
<label>
|
||||
<Input type="checkbox" style={{width: 'inherit', marginRight: '10px'}} onChange={(e) => this.enableLog(e.target.checked)} defaultChecked={this.state.enable_telemetry} />
|
||||
I accept but the data is not to be share with any third party
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
), () => this.unlisten());
|
||||
});
|
||||
});
|
||||
|
||||
bcrypt_password(this.state.password)
|
||||
.then((hash) => {
|
||||
config.auth.admin.value = hash;
|
||||
config = FormObjToJSON(config);
|
||||
config.connections = window.CONFIG.connections;
|
||||
Config.save(config, false)
|
||||
.then(() => Admin.login(this.state.password))
|
||||
.then(() => this.setState({stage: 1, creating_password: false}))
|
||||
.catch((err) => {
|
||||
notify.send(err && err.message, "error");
|
||||
this.setState({creating_password: false});
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
notify.send("Hash error: " + JSON.stringify(err), "error");
|
||||
this.setState({creating_password: false});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
enableLog(value){
|
||||
Config.all().then((config) => {
|
||||
config.log.telemetry.value = value;
|
||||
config = FormObjToJSON(config);
|
||||
config.connections = window.CONFIG.connections;
|
||||
Config.save(config, false);
|
||||
});
|
||||
}
|
||||
|
||||
start(e){
|
||||
e.preventDefault();
|
||||
this.props.history.push("/");
|
||||
}
|
||||
|
||||
renderStage(stage){
|
||||
if(stage === 0){
|
||||
return (
|
||||
<div>
|
||||
<h2>You made it chief! { this.state.creating_password === true ? <Icon style={{height: '40px'}} name="loading"/> : null}</h2>
|
||||
<p>
|
||||
Let's start by protecting the admin area with a password:
|
||||
</p>
|
||||
<form onSubmit={this.createPassword.bind(this)} style={{maxWidth: '350px'}}>
|
||||
<Input type="password" placeholder="Create your admin password" defaultValue="" onChange={(e) => this.setState({password: e.target.value})} autoComplete="new-password"/>
|
||||
<Button className="primary">Create Password</Button>
|
||||
</form>
|
||||
<style dangerouslySetInnerHTML={{__html: ".component_menu_sidebar{transform: translateX(-300px)}"}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h2>Welcome to the engine room</h2>
|
||||
<p>
|
||||
This is the place where you can configure filestash to your liking. Feel free to poke around. <br/>
|
||||
You can come back by navigating at <a href="/admin">`{window.location.origin + "/admin"}`</a>. <br/>
|
||||
Have fun!
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div className="component_setup">
|
||||
{ this.renderStage(this.state.stage) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
client/pages/adminpage/setup.scss
Normal file
17
client/pages/adminpage/setup.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.component_setup{
|
||||
transform: none!important; // transition and fixed posiiton doesn't cohabit well so we have to resort
|
||||
// to remove animation on this page to preserve the layout
|
||||
text-align: justify;
|
||||
|
||||
button.completed{
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 150px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 10px;
|
||||
font-size: 1.1em;
|
||||
color: var(--emphasis);
|
||||
}
|
||||
}
|
||||
28
client/pages/adminpage/support.js
Normal file
28
client/pages/adminpage/support.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
export class SupportPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<h2>Support</h2>
|
||||
<p>
|
||||
<a href="mailto:mickael@kerjean.me">contact us</a> directly if you have/want enterprise support
|
||||
</p>
|
||||
<p>
|
||||
There's also a community chat on Freenode - #filestash (or click <a href="https://kiwiirc.com/nextclient/#irc://irc.freenode.net/#filestash?nick=guest??">here</a> if you're not an IRC guru).
|
||||
</p>
|
||||
<h2>Quick Links</h2>
|
||||
<ul>
|
||||
<li><a href="https://www.filestash.app/support#faq">FAQ</a></li>
|
||||
<li><a href="https://www.filestash.app/docs">Documentation</a></li>
|
||||
<li><a href="https://www.filestash.app/">Our website</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -48,7 +48,7 @@ export class ConnectPage extends React.Component {
|
||||
.then(Session.currentUser)
|
||||
.then((user) => {
|
||||
let url = '/files/';
|
||||
let path = user.home
|
||||
let path = user.home;
|
||||
if(path){
|
||||
path = path.replace(/^\/?(.*?)\/?$/, "$1");
|
||||
if(path !== ""){
|
||||
@ -108,7 +108,7 @@ export class ConnectPage extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="component_page_connect">
|
||||
<NgIf cond={CONFIG["fork_button"]}>
|
||||
<NgIf cond={window.CONFIG["fork_button"]}>
|
||||
<ForkMe repo="https://github.com/mickael-kerjean/nuage" />
|
||||
</NgIf>
|
||||
<Container maxWidth="565px">
|
||||
|
||||
@ -47,10 +47,12 @@
|
||||
|
||||
.component_page_connection_form.form-appear{
|
||||
opacity: 0;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
.component_page_connection_form.form-appear.form-appear-active{
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-out;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.25s ease-out, opacity 0.5s ease-out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { NgIf, Icon } from '../../components/';
|
||||
import { NgIf, Icon, Button } from '../../components/';
|
||||
import { Share } from '../../model/';
|
||||
import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/';
|
||||
import './share.scss';
|
||||
|
||||
@ -82,7 +82,6 @@ export class NewThing extends React.Component {
|
||||
|
||||
onSearchChange(search){
|
||||
this.setState({search_keyword: search});
|
||||
console.log(search);
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
@ -25,9 +25,7 @@ export class HomePage extends React.Component {
|
||||
this.setState({redirection: "/login"});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setState({redirection: "/login"});
|
||||
});
|
||||
.catch((err) => this.setState({redirection: "/login"}));
|
||||
}
|
||||
render() {
|
||||
if(this.state.redirection !== null){
|
||||
|
||||
@ -60,9 +60,6 @@ export class Pager extends React.Component {
|
||||
this.props.history.push(url);
|
||||
if(this.refs.$page) this.refs.$page.blur();
|
||||
let preload_index = (n >= this.state.n || (this.state.n === this.state.files.length - 1 && n === 0)) ? this.calculateNextPageNumber(n) : this.calculatePrevPageNumber(n);
|
||||
if(!this.state.files[preload_index].path){
|
||||
console.log("> ISSUE: ", this.state.files[preload_index]);
|
||||
}
|
||||
Files.url(this.state.files[preload_index].path)
|
||||
.then((url) => this.props.emit("media::preload", url))
|
||||
.catch(() => {});
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
|
||||
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
|
||||
import { Bundle, URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
|
||||
import { ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
|
||||
import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
|
||||
import { Bundle, ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
|
||||
|
||||
const AdminPage = (props) => (
|
||||
<Bundle loader={import(/* webpackChunkName: "admin" */"./pages/adminpage")} symbol="AdminPage">
|
||||
{(Comp) => <Comp {...props}/>}
|
||||
</Bundle>
|
||||
);
|
||||
|
||||
export default class AppRouter extends React.Component {
|
||||
render() {
|
||||
@ -16,6 +22,7 @@ export default class AppRouter extends React.Component {
|
||||
<Route path="/files/:path*" component={FilesPage} />
|
||||
<Route path="/view/:path*" component={ViewerPage} />
|
||||
<Route path="/logout" component={LogoutPage} />
|
||||
<Route path="/admin" component={AdminPage} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.11.4",
|
||||
"babel-core": "^6.13.2",
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package common
|
||||
|
||||
type App struct {
|
||||
Config *Config
|
||||
Backend IBackend
|
||||
Body map[string]interface{}
|
||||
Session map[string]string
|
||||
|
||||
@ -36,6 +36,10 @@ func (d *Driver) Get(name string) IBackend {
|
||||
return b
|
||||
}
|
||||
|
||||
func (d *Driver) Drivers() map[string]IBackend {
|
||||
return d.ds
|
||||
}
|
||||
|
||||
type Nothing struct {}
|
||||
|
||||
func (b Nothing) Init(params map[string]string, app *App) (IBackend, error) {
|
||||
@ -65,3 +69,7 @@ func (b Nothing) Touch(path string) error {
|
||||
func (b Nothing) Save(path string, file io.Reader) error {
|
||||
return NewError("", 401)
|
||||
}
|
||||
|
||||
func (b Nothing) LoginForm() Form {
|
||||
return Form{}
|
||||
}
|
||||
|
||||
@ -48,3 +48,27 @@ func NewAppCache(arg ...time.Duration) AppCache {
|
||||
c.Cache = cache.New(retention*time.Minute, cleanup*time.Minute)
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
|
||||
|
||||
type KeyValueStore struct {
|
||||
cache map[string]interface{}
|
||||
}
|
||||
|
||||
func NewKeyValueStore() KeyValueStore {
|
||||
return KeyValueStore{ cache: make(map[string]interface{}) }
|
||||
}
|
||||
|
||||
func (this KeyValueStore) Get(key string) interface{} {
|
||||
return this.cache[key]
|
||||
}
|
||||
|
||||
func (this *KeyValueStore) Set(key string, value interface{}) {
|
||||
this.cache[key] = value
|
||||
}
|
||||
|
||||
func (this *KeyValueStore) Clear() {
|
||||
this.cache = make(map[string]interface{})
|
||||
}
|
||||
|
||||
@ -1,183 +1,322 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var SECRET_KEY string
|
||||
var configPath string = filepath.Join(GetCurrentDir(), CONFIG_PATH + "config.json")
|
||||
var (
|
||||
Config Configuration
|
||||
configPath string = filepath.Join(GetCurrentDir(), CONFIG_PATH + "config.json")
|
||||
SECRET_KEY string
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
mu sync.Mutex
|
||||
currentElement *FormElement
|
||||
cache KeyValueStore
|
||||
form []Form
|
||||
conn []map[string]interface{}
|
||||
}
|
||||
|
||||
type Form struct {
|
||||
Title string
|
||||
Form []Form
|
||||
Elmnts []FormElement
|
||||
}
|
||||
|
||||
type FormElement struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Opts []string `json:"options,omitempty"`
|
||||
Target []string `json:"target,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Default interface{} `json:"default"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
c := NewConfig()
|
||||
// Let's initialise all our json config stuff
|
||||
// For some reasons the file will be written bottom up so we start from the end moving up to the top
|
||||
Config = NewConfiguration()
|
||||
Config.Load()
|
||||
Config.Init()
|
||||
}
|
||||
|
||||
// Connections
|
||||
if c.Get("connections.0.type").Interface() == nil {
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "webdav", "label": "Webdav"})
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "ftp", "label": "FTP"})
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "sftp", "label": "SFTP"})
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "git", "label": "GIT"})
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "s3", "label": "S3"})
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "dropbox", "label": "Dropbox"})
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "gdrive", "label": "Drive"})
|
||||
func NewConfiguration() Configuration {
|
||||
return Configuration{
|
||||
mu: sync.Mutex{},
|
||||
cache: NewKeyValueStore(),
|
||||
form: []Form{
|
||||
Form{
|
||||
Title: "general",
|
||||
Elmnts: []FormElement{
|
||||
FormElement{Name: "name", Type: "text", Default: "Nuage", Description: "Name has shown in the UI", Placeholder: "Default: \"Filestash\""},
|
||||
FormElement{Name: "port", Type: "number", Default: 8334, Description: "Port on which the application is available.", Placeholder: "Default: 8334"},
|
||||
FormElement{Name: "host", Type: "text", Default: "https://demo.filestash.app", Description: "The URL that users will use", Placeholder: "Eg: \"https://demo.filestash.app\""},
|
||||
FormElement{Name: "secret_key", Type: "password", Description: "The key that's used to encrypt and decrypt content. Update this settings will invalidate existing user sessions and shared links, use with caution!"},
|
||||
FormElement{Name: "editor", Type: "select", Default: "emacs", Opts: []string{"base", "emacs", "vim"}, Description: "Keybinding to be use in the editor. Default: \"emacs\""},
|
||||
FormElement{Name: "fork_button", Type: "boolean", Default: true, Description: "Display the fork button in the login screen"},
|
||||
FormElement{Name: "display_hidden", Type: "boolean", Default: false, Description: "Should files starting with a dot be visible by default?"},
|
||||
FormElement{Name: "auto_connect", Type: "boolean", Default: false, Description: "User don't have to click on the login button if an admin is prefilling a unique backend"},
|
||||
FormElement{Name: "remember_me", Type: "boolean", Default: true, Description: "Visiblity of the remember me button on the login screen"},
|
||||
},
|
||||
},
|
||||
Form{
|
||||
Title: "features",
|
||||
Form: []Form{
|
||||
Form{
|
||||
Title: "search",
|
||||
Elmnts: []FormElement{
|
||||
FormElement{Name: "enable", Type: "boolean", Default: true, Description: "Enable/Disable the search feature"},
|
||||
},
|
||||
},
|
||||
Form{
|
||||
Title: "share",
|
||||
Elmnts: []FormElement{
|
||||
FormElement{Name: "enable", Type: "boolean", Default: true, Description: "Enable/Disable the share feature"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Form{
|
||||
Title: "log",
|
||||
Elmnts: []FormElement{
|
||||
FormElement{Name: "enable", Type: "enable", Target: []string{"log_level"}, Default: true},
|
||||
FormElement{Name: "level", Type: "select", Default: "INFO", Opts: []string{"DEBUG", "INFO", "WARNING", "ERROR"}, Id: "log_level", Description: "Default: \"INFO\". This setting determines the level of detail at which log events are written to the log file"},
|
||||
FormElement{Name: "telemetry", Type: "boolean", Default: false, Description: "We won't share anything with any third party. This will only to be used to improve Filestash"},
|
||||
},
|
||||
},
|
||||
Form{
|
||||
Title: "email",
|
||||
Elmnts: []FormElement{
|
||||
FormElement{Name: "server", Type: "text", Default: "smtp.gmail.com", Description: "Address of the SMTP email server.", Placeholder: "Default: smtp.gmail.com"},
|
||||
FormElement{Name: "port", Type: "number", Default: 587, Description: "Port of the SMTP email server. Eg: 587", Placeholder: "Default: 587"},
|
||||
FormElement{Name: "username", Type: "text", Description: "The username for authenticating to the SMTP server.", Placeholder: "Eg: username@gmail.com"},
|
||||
FormElement{Name: "password", Type: "password", Description: "The password associated with the SMTP username.", Placeholder: "Eg: Your google password"},
|
||||
FormElement{Name: "from", Type: "text", Description: "Email address visible on sent messages.", Placeholder: "Eg: username@gmail.com"},
|
||||
},
|
||||
},
|
||||
Form{
|
||||
Title: "auth",
|
||||
Elmnts: []FormElement{
|
||||
FormElement{Name: "admin", Type: "bcrypt", Default: "", Description: "Password of the admin section."},
|
||||
},
|
||||
Form: []Form{
|
||||
Form{
|
||||
Title: "custom",
|
||||
Elmnts: []FormElement{
|
||||
FormElement{Name: "client_secret", Type: "password"},
|
||||
FormElement{Name: "client_id", Type: "text"},
|
||||
FormElement{Name: "sso_domain", Type: "text"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
conn: make([]map[string]interface{}, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (this Form) MarshalJSON() ([]byte, error) {
|
||||
return []byte(this.toJSON(func(el FormElement) string {
|
||||
a, e := json.Marshal(el)
|
||||
if e != nil {
|
||||
return ""
|
||||
}
|
||||
return string(a)
|
||||
})), nil
|
||||
}
|
||||
|
||||
func (this Form) toJSON(fn func(el FormElement) string) string {
|
||||
formatKey := func(str string) string {
|
||||
str = strings.ToLower(str)
|
||||
return strings.Replace(str, " ", "_", -1)
|
||||
}
|
||||
ret := ""
|
||||
if this.Title != "" {
|
||||
ret = fmt.Sprintf("%s\"%s\":", ret, formatKey(this.Title))
|
||||
}
|
||||
for i := 0; i < len(this.Elmnts); i++ {
|
||||
if i == 0 {
|
||||
ret = fmt.Sprintf("%s{", ret)
|
||||
}
|
||||
ret = fmt.Sprintf("%s\"%s\":%s", ret, formatKey(this.Elmnts[i].Name), fn(this.Elmnts[i]))
|
||||
if i == len(this.Elmnts) - 1 && len(this.Form) == 0 {
|
||||
ret = fmt.Sprintf("%s}", ret)
|
||||
}
|
||||
if i != len(this.Elmnts) - 1 || len(this.Form) != 0 {
|
||||
ret = fmt.Sprintf("%s,", ret)
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth credentials
|
||||
c.Get("oauth").Default("")
|
||||
|
||||
// Features
|
||||
c.Get("features.share.enable").Default(true)
|
||||
c.Get("features.search.enable").Default(true)
|
||||
|
||||
|
||||
// Log
|
||||
c.Get("log.telemetry").Default(true)
|
||||
c.Get("log.level").Default("INFO")
|
||||
c.Get("log.enable").Default(true)
|
||||
|
||||
// Email
|
||||
c.Get("email.from").Default("username@gmail.com")
|
||||
c.Get("email.password").Default("password")
|
||||
c.Get("email.username").Default("username@gmail.com")
|
||||
c.Get("email.port").Default(587)
|
||||
c.Get("email.server").Default("smtp.gmail.com")
|
||||
|
||||
// General
|
||||
c.Get("general.remember_me").Default(true)
|
||||
c.Get("general.auto_connect").Default(false)
|
||||
c.Get("general.display_hidden").Default(true)
|
||||
c.Get("general.fork_button").Default(true)
|
||||
c.Get("general.editor").Default("emacs")
|
||||
if c.Get("general.secret_key").String() == "" {
|
||||
c.Get("general.secret_key").Set(RandomString(16))
|
||||
for i := 0; i < len(this.Form); i++ {
|
||||
if i == 0 && len(this.Elmnts) == 0 {
|
||||
ret = fmt.Sprintf("%s{", ret)
|
||||
}
|
||||
ret = ret + this.Form[i].toJSON(fn)
|
||||
if i == len(this.Form) - 1 {
|
||||
ret = fmt.Sprintf("%s}", ret)
|
||||
}
|
||||
if i != len(this.Form) - 1 {
|
||||
ret = fmt.Sprintf("%s,", ret)
|
||||
}
|
||||
}
|
||||
|
||||
if len(this.Form) == 0 && len(this.Elmnts) == 0 {
|
||||
ret = fmt.Sprintf("%s{}", ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type FormIterator struct {
|
||||
Path string
|
||||
*FormElement
|
||||
}
|
||||
func (this *Form) Iterator() []FormIterator {
|
||||
slice := make([]FormIterator, 0)
|
||||
|
||||
for i, _ := range this.Elmnts {
|
||||
slice = append(slice, FormIterator{
|
||||
strings.ToLower(this.Title),
|
||||
&this.Elmnts[i],
|
||||
})
|
||||
}
|
||||
for _, node := range this.Form {
|
||||
r := node.Iterator()
|
||||
if this.Title != "" {
|
||||
for i := range r {
|
||||
r[i].Path = strings.ToLower(this.Title) + "." + r[i].Path
|
||||
}
|
||||
}
|
||||
slice = append(r, slice...)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func (this *Configuration) Load() {
|
||||
file, err := os.OpenFile(configPath, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
Log.Warning("Can't read from config file")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
cFile, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
Log.Warning("Can't parse config file")
|
||||
return
|
||||
}
|
||||
|
||||
this.conn = func(cFile []byte) []map[string]interface{} {
|
||||
var d struct {
|
||||
Connections []map[string]interface{} `json:"connections"`
|
||||
}
|
||||
json.Unmarshal(cFile, &d)
|
||||
return d.Connections
|
||||
}(cFile)
|
||||
|
||||
this.form = func(cFile []byte) []Form {
|
||||
f := Form{Form: this.form}
|
||||
for _, el := range f.Iterator() {
|
||||
value := gjson.Get(string(cFile), el.Path + "." + el.Name).Value()
|
||||
if value != nil {
|
||||
el.Value = value
|
||||
}
|
||||
}
|
||||
return this.form
|
||||
}(cFile)
|
||||
|
||||
Log.SetVisibility(this.Get("log.level").String())
|
||||
return
|
||||
}
|
||||
|
||||
func (this *Configuration) Init() {
|
||||
if this.Get("general.secret_key").String() == "" {
|
||||
key := RandomString(16)
|
||||
this.Get("general.secret_key").Set(key)
|
||||
}
|
||||
SECRET_KEY = c.Get("general.secret_key").String()
|
||||
if env := os.Getenv("APPLICATION_URL"); env != "" {
|
||||
c.Get("general.host").Set(env)
|
||||
} else {
|
||||
c.Get("general.host").Default("http://127.0.0.1:8334")
|
||||
this.Get("general.host").Set(env).String()
|
||||
}
|
||||
c.Get("general.port").Default(8334)
|
||||
c.Get("general.name").Default("Nuage")
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
a := Config{}
|
||||
return a.load()
|
||||
}
|
||||
type Config struct {
|
||||
mu sync.Mutex
|
||||
path *string
|
||||
json string
|
||||
reader gjson.Result
|
||||
}
|
||||
|
||||
func (this *Config) load() *Config {
|
||||
if f, err := os.OpenFile(configPath, os.O_RDONLY, os.ModePerm); err == nil {
|
||||
j, _ := ioutil.ReadAll(f)
|
||||
this.json = string(j)
|
||||
f.Close()
|
||||
} else {
|
||||
this.json = `{}`
|
||||
if len(this.conn) == 0 {
|
||||
this.conn = []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "webdav",
|
||||
"label": "WebDav",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "ftp",
|
||||
"label": "FTP",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "sftp",
|
||||
"label": "SFTP",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "git",
|
||||
"label": "GIT",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "s3",
|
||||
"label": "S3",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "dropbox",
|
||||
"label": "Dropbox",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "gdrive",
|
||||
"label": "Drive",
|
||||
},
|
||||
}
|
||||
this.Save()
|
||||
}
|
||||
if gjson.Valid(this.json) == true {
|
||||
this.reader = gjson.Parse(this.json)
|
||||
}
|
||||
return this
|
||||
SECRET_KEY = this.Get("general.secret_key").String()
|
||||
}
|
||||
|
||||
func (this *Config) Get(path string) *Config {
|
||||
this.path = &path
|
||||
return this
|
||||
}
|
||||
func (this Configuration) Save() Configuration {
|
||||
// convert config data to an appropriate json struct
|
||||
v := Form{Form: this.form}.toJSON(func (el FormElement) string {
|
||||
a, e := json.Marshal(el.Value)
|
||||
if e != nil {
|
||||
return "null"
|
||||
}
|
||||
return string(a)
|
||||
})
|
||||
|
||||
func (this *Config) Default(value interface{}) *Config {
|
||||
if this.path == nil {
|
||||
// convert back to a map[string]interface{} so that we can stuff in backends config
|
||||
var tmp map[string]interface{}
|
||||
json.Unmarshal([]byte(v), &tmp)
|
||||
tmp["connections"] = this.conn
|
||||
|
||||
// let's build a json of the whole struct
|
||||
j, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
return this
|
||||
}
|
||||
|
||||
if val := this.reader.Get(*this.path).Value(); val == nil {
|
||||
this.mu.Lock()
|
||||
this.json, _ = sjson.Set(this.json, *this.path, value)
|
||||
this.reader = gjson.Parse(this.json)
|
||||
this.save()
|
||||
this.mu.Unlock()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Config) Set(value interface{}) *Config {
|
||||
if this.path == nil {
|
||||
// deploy the config in our config.json
|
||||
file, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
return this
|
||||
}
|
||||
|
||||
this.mu.Lock()
|
||||
this.json, _ = sjson.Set(this.json, *this.path, value)
|
||||
this.reader = gjson.Parse(this.json)
|
||||
this.save()
|
||||
this.mu.Unlock()
|
||||
defer file.Close()
|
||||
file.Write(PrettyPrint(j))
|
||||
return this
|
||||
}
|
||||
|
||||
func (this Config) String() string {
|
||||
return this.reader.Get(*this.path).String()
|
||||
}
|
||||
|
||||
func (this Config) Int() int {
|
||||
val := this.reader.Get(*this.path).Value()
|
||||
switch val.(type) {
|
||||
case float64: return int(val.(float64))
|
||||
case int64: return int(val.(int64))
|
||||
case int: return val.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (this Config) Bool() bool {
|
||||
val := this.reader.Get(*this.path).Value()
|
||||
switch val.(type) {
|
||||
case bool: return val.(bool)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (this Config) Interface() interface{} {
|
||||
return this.reader.Get(*this.path).Value()
|
||||
}
|
||||
|
||||
func (this Config) save() {
|
||||
if this.path == nil {
|
||||
Log.Error("Config error")
|
||||
return
|
||||
}
|
||||
if gjson.Valid(this.json) == false {
|
||||
Log.Error("Config error")
|
||||
return
|
||||
}
|
||||
if f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE, os.ModePerm); err == nil {
|
||||
buf := bytes.NewBuffer(PrettyPrint([]byte(this.json)))
|
||||
io.Copy(f, buf)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (this Config) Scan(p interface{}) error {
|
||||
content := this.reader.Get(*this.path).String()
|
||||
|
||||
return json.Unmarshal([]byte(content), &p)
|
||||
}
|
||||
|
||||
func (this Config) Export() (string, error) {
|
||||
publicConf := struct {
|
||||
func (this Configuration) Export() interface{} {
|
||||
return struct {
|
||||
Editor string `json:"editor"`
|
||||
ForkButton bool `json:"fork_button"`
|
||||
DisplayHidden bool `json:"display_hidden"`
|
||||
@ -195,14 +334,127 @@ func (this Config) Export() (string, error) {
|
||||
AutoConnect: this.Get("general.auto_connect").Bool(),
|
||||
Name: this.Get("general.name").String(),
|
||||
RememberMe: this.Get("general.remember_me").Bool(),
|
||||
Connections: this.Get("connections").Interface(),
|
||||
Connections: this.conn,
|
||||
EnableSearch: this.Get("features.search.enable").Bool(),
|
||||
EnableShare: this.Get("features.share.enable").Bool(),
|
||||
MimeTypes: AllMimeTypes(),
|
||||
}
|
||||
j, err := json.Marshal(publicConf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(j), nil
|
||||
}
|
||||
|
||||
func (this *Configuration) Get(key string) *Configuration {
|
||||
var traverse func (forms *[]Form, path []string) *FormElement
|
||||
traverse = func (forms *[]Form, path []string) *FormElement {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := range *forms {
|
||||
currentForm := (*forms)[i]
|
||||
if currentForm.Title == path[0] {
|
||||
if len(path) == 2 {
|
||||
// we are on a leaf
|
||||
// 1) attempt to get a `formElement`
|
||||
for j, el := range currentForm.Elmnts {
|
||||
if el.Name == path[1] {
|
||||
return &(*forms)[i].Elmnts[j]
|
||||
}
|
||||
}
|
||||
// 2) `formElement` does not exist, let's create it
|
||||
(*forms)[i].Elmnts = append(currentForm.Elmnts, FormElement{ Name: path[1], Type: "text" })
|
||||
return &(*forms)[i].Elmnts[len(currentForm.Elmnts)]
|
||||
} else {
|
||||
// we are NOT on a leaf, let's continue our tree transversal
|
||||
return traverse(&(*forms)[i].Form, path[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
// append a new `form` if the current key doesn't exist
|
||||
*forms = append(*forms, Form{ Title: path[0] })
|
||||
return traverse(forms, path)
|
||||
}
|
||||
|
||||
// increase speed (x4 with our bench) by using a cache
|
||||
tmp := this.cache.Get(key)
|
||||
if tmp == nil {
|
||||
this.currentElement = traverse(&this.form, strings.Split(key, "."))
|
||||
this.cache.Set(key, this.currentElement)
|
||||
} else {
|
||||
this.currentElement = tmp.(*FormElement)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Configuration) Default(value interface{}) *Configuration {
|
||||
if this.currentElement == nil {
|
||||
return this
|
||||
}
|
||||
|
||||
this.mu.Lock()
|
||||
if this.currentElement.Default == nil {
|
||||
this.currentElement.Default = value
|
||||
this.Save()
|
||||
} else {
|
||||
if this.currentElement.Default != value {
|
||||
Log.Debug("Attempt to set multiple default config value => %+v", this.currentElement)
|
||||
}
|
||||
}
|
||||
this.mu.Unlock()
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Configuration) Set(value interface{}) *Configuration {
|
||||
if this.currentElement == nil {
|
||||
return this
|
||||
}
|
||||
|
||||
this.mu.Lock()
|
||||
if this.currentElement.Value != value {
|
||||
this.currentElement.Value = value
|
||||
this.Save()
|
||||
}
|
||||
this.mu.Unlock()
|
||||
return this
|
||||
}
|
||||
|
||||
func (this Configuration) String() string {
|
||||
val := this.Interface()
|
||||
switch val.(type) {
|
||||
case string: return val.(string)
|
||||
case []byte: return string(val.([]byte))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this Configuration) Int() int {
|
||||
val := this.Interface()
|
||||
switch val.(type) {
|
||||
case float64: return int(val.(float64))
|
||||
case int64: return int(val.(int64))
|
||||
case int: return val.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (this Configuration) Bool() bool {
|
||||
val := this.Interface()
|
||||
switch val.(type) {
|
||||
case bool: return val.(bool)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (this Configuration) Interface() interface{} {
|
||||
if this.currentElement == nil {
|
||||
return nil
|
||||
}
|
||||
val := this.currentElement.Value
|
||||
if val == nil {
|
||||
val = this.currentElement.Default
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (this Configuration) MarshalJSON() ([]byte, error) {
|
||||
return Form{
|
||||
Form: this.form,
|
||||
}.MarshalJSON()
|
||||
}
|
||||
|
||||
@ -6,12 +6,14 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigGet(t *testing.T) {
|
||||
assert.Equal(t, nil, NewConfig().Get("foo").Interface())
|
||||
assert.Equal(t, nil, NewConfig().Get("foo.bar").Interface())
|
||||
c := NewConfiguration()
|
||||
assert.Equal(t, nil, c.Get("foo").Interface())
|
||||
assert.Equal(t, nil, c.Get("foo.bar").Interface())
|
||||
}
|
||||
|
||||
func TestConfigDefault(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := NewConfiguration()
|
||||
assert.Equal(t, "test", c.Get("foo.bar").Default("test").Interface())
|
||||
assert.Equal(t, "test", c.Get("foo.bar").Default("test").String())
|
||||
assert.Equal(t, "test", c.Get("foo.bar").String())
|
||||
assert.Equal(t, "test", c.Get("foo.bar").Default("nope").String())
|
||||
@ -19,59 +21,25 @@ func TestConfigDefault(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigTypeCase(t *testing.T) {
|
||||
assert.Equal(t, nil, NewConfig().Get("foo.bar.nil").Default(nil).Interface())
|
||||
assert.Equal(t, true, NewConfig().Get("foo.bar").Default(true).Bool())
|
||||
assert.Equal(t, 10, NewConfig().Get("foo.bar").Default(10).Int())
|
||||
assert.Equal(t, "test", NewConfig().Get("foo.bar").Default("test").String())
|
||||
c := NewConfiguration()
|
||||
assert.Equal(t, nil, c.Get("foo.bar.nil").Default(nil).Interface())
|
||||
assert.Equal(t, true, c.Get("foo.bar.bool").Default(true).Bool())
|
||||
assert.Equal(t, 100, c.Get("foo.bar.int").Default(100).Int())
|
||||
assert.Equal(t, "test", c.Get("foo.bar.string").Default("test").String())
|
||||
}
|
||||
|
||||
func TestConfigSet(t *testing.T) {
|
||||
c := NewConfig()
|
||||
assert.Equal(t, "test", c.Get("foo.bar").Set("test").String())
|
||||
assert.Equal(t, "valu", c.Get("foo.bar").Set("valu").String())
|
||||
assert.Equal(t, "valu", c.Get("foo.bar.test.bar.foo").Set("valu").String())
|
||||
}
|
||||
|
||||
func TestConfigScan(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c.Get("foo.bar").Default("test")
|
||||
c.Get("foo.bar2").Default(32)
|
||||
c.Get("foo.bar3").Default(true)
|
||||
|
||||
var data struct {
|
||||
Bar string `json:"bar"`
|
||||
Bar2 int `json:"bar2"`
|
||||
Bar3 bool `json:"bar3"`
|
||||
}
|
||||
c.Get("foo").Scan(&data)
|
||||
assert.Equal(t, "test", data.Bar)
|
||||
assert.Equal(t, 32, data.Bar2)
|
||||
assert.Equal(t, true, data.Bar3)
|
||||
}
|
||||
|
||||
func TestConfigSlice(t *testing.T) {
|
||||
c := NewConfig()
|
||||
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "test0", "label": "test0"})
|
||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "test1", "label": "Test1"})
|
||||
|
||||
var data []struct {
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
c.Get("connections").Scan(&data)
|
||||
assert.Equal(t, 2, len(data))
|
||||
assert.Equal(t, "test0", data[0].Type)
|
||||
assert.Equal(t, "test0", data[0].Label)
|
||||
assert.Equal(t, "test", Config.Get("foo.bar").Set("test").String())
|
||||
assert.Equal(t, "valu", Config.Get("foo.bar").Set("valu").String())
|
||||
assert.Equal(t, "valu", Config.Get("foo.bar.test.bar.foo").Set("valu").String())
|
||||
}
|
||||
|
||||
func BenchmarkGetConfigElement(b *testing.B) {
|
||||
c := NewConfig()
|
||||
c := NewConfiguration()
|
||||
c.Get("foo.bar.test.foo").Set("test")
|
||||
c.Get("foo.bar.test.bar.foo").Set("valu")
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
c.Get("foo").String()
|
||||
c.Get("foo.bar.test.foo").String()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,9 +4,13 @@ const (
|
||||
APP_VERSION = "v0.3"
|
||||
CONFIG_PATH = "data/config/"
|
||||
PLUGIN_PATH = "data/plugin/"
|
||||
LOG_PATH = "data/log/"
|
||||
COOKIE_NAME_AUTH = "auth"
|
||||
COOKIE_NAME_PROOF = "proof"
|
||||
COOKIE_NAME_ADMIN = "admin"
|
||||
COOKIE_PATH_ADMIN = "/admin/api/"
|
||||
COOKIE_PATH = "/api/"
|
||||
FILE_INDEX = "./data/public/index.html"
|
||||
FILE_ASSETS = "./data/public/"
|
||||
URL_SETUP = "/admin/setup"
|
||||
)
|
||||
|
||||
@ -142,7 +142,7 @@ func verify(something []byte) ([]byte, error) {
|
||||
func GenerateID(ctx *App) string {
|
||||
params := ctx.Session
|
||||
p := "type =>" + params["type"]
|
||||
p += "salt => " + ctx.Config.Get("general.secret_key").String()
|
||||
p += "salt => " + SECRET_KEY
|
||||
p += "host =>" + params["host"]
|
||||
p += "hostname =>" + params["hostname"]
|
||||
p += "username =>" + params["username"]
|
||||
|
||||
@ -24,7 +24,6 @@ func TestIDGeneration(t *testing.T) {
|
||||
session["foo"] = "bar"
|
||||
app := &App{
|
||||
Session: session,
|
||||
Config: NewConfig(),
|
||||
}
|
||||
|
||||
id1 := GenerateID(app)
|
||||
|
||||
@ -14,6 +14,7 @@ var (
|
||||
ErrPermissionDenied error = NewError("Permission Denied", 403)
|
||||
ErrNotValid error = NewError("Not Valid", 405)
|
||||
ErrNotReachable error = NewError("Cannot Reach Destination", 502)
|
||||
ErrInvalidPassword = NewError("Invalid Password", 403)
|
||||
)
|
||||
|
||||
type AppError struct {
|
||||
|
||||
@ -2,9 +2,25 @@ package common
|
||||
|
||||
import (
|
||||
slog "log"
|
||||
"fmt"
|
||||
"time"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var logfile *os.File
|
||||
|
||||
func init(){
|
||||
var err error
|
||||
logPath := filepath.Join(GetCurrentDir(), LOG_PATH)
|
||||
os.MkdirAll(logPath, os.ModePerm)
|
||||
logfile, err = os.OpenFile(filepath.Join(logPath, "access.log"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
slog.Printf("ERROR log file: %+v", err)
|
||||
}
|
||||
logfile.WriteString("")
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Host string `json:"host"`
|
||||
Method string `json:"method"`
|
||||
@ -32,28 +48,53 @@ type log struct{
|
||||
|
||||
func (l *log) Info(format string, v ...interface{}) {
|
||||
if l.info && l.enable {
|
||||
slog.Printf("INFO " + format + "\n", v...)
|
||||
message := fmt.Sprintf("%s INFO ", l.now())
|
||||
message = fmt.Sprintf(message + format + "\n", v...)
|
||||
logfile.WriteString(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *log) Warning(format string, v ...interface{}) {
|
||||
if l.warn && l.enable {
|
||||
slog.Printf("WARN " + format + "\n", v...)
|
||||
message := fmt.Sprintf("%s WARN ", l.now())
|
||||
message = fmt.Sprintf(message + format + "\n", v...)
|
||||
logfile.WriteString(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *log) Error(format string, v ...interface{}) {
|
||||
if l.error && l.enable {
|
||||
slog.Printf("ERROR " + format + "\n", v...)
|
||||
message := fmt.Sprintf("%s ERROR ", l.now())
|
||||
message = fmt.Sprintf(message + format + "\n", v...)
|
||||
logfile.WriteString(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *log) Debug(format string, v ...interface{}) {
|
||||
if l.debug && l.enable {
|
||||
slog.Printf("DEBUG " + format + "\n", v...)
|
||||
message := fmt.Sprintf("%s DEBUG ", l.now())
|
||||
message = fmt.Sprintf(message + format + "\n", v...)
|
||||
logfile.WriteString(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *log) Stdout(format string, v ...interface{}) {
|
||||
slog.Printf(format + "\n", v...)
|
||||
if l.enable {
|
||||
message := fmt.Sprintf("%s MESSAGE: ", l.now())
|
||||
message = fmt.Sprintf(message + format + "\n", v...)
|
||||
logfile.WriteString(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *log) now() string {
|
||||
return time.Now().Format("2006/01/02 15:04:05")
|
||||
}
|
||||
|
||||
func (l *log) Close() {
|
||||
logfile.Close()
|
||||
}
|
||||
|
||||
func (l *log) SetVisibility(str string) {
|
||||
switch str {
|
||||
case "WARNING":
|
||||
|
||||
@ -5,6 +5,17 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginTypeBackend = "backend"
|
||||
PluginTypeMiddleware = "middleware"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
Type string
|
||||
Enable bool
|
||||
}
|
||||
|
||||
|
||||
type Register struct{}
|
||||
type Get struct{}
|
||||
|
||||
|
||||
35
server/common/token.go
Normal file
35
server/common/token.go
Normal file
@ -0,0 +1,35 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ADMIN_CLAIM = "ADMIN"
|
||||
)
|
||||
|
||||
type AdminToken struct {
|
||||
Claim string `json:"token"`
|
||||
Expire time.Time `json:"time"`
|
||||
}
|
||||
|
||||
func NewAdminToken() AdminToken {
|
||||
return AdminToken{
|
||||
Claim: ADMIN_CLAIM,
|
||||
Expire: time.Now().Add(time.Hour * 24),
|
||||
}
|
||||
}
|
||||
|
||||
func (this AdminToken) IsAdmin() bool {
|
||||
if this.Claim != ADMIN_CLAIM {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (this AdminToken) IsValid() bool {
|
||||
if this.Expire.Sub(time.Now()) <= 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -17,6 +17,7 @@ type IBackend interface {
|
||||
Save(path string, file io.Reader) error
|
||||
Touch(path string) error
|
||||
Info() string
|
||||
LoginForm() Form
|
||||
}
|
||||
|
||||
type File struct {
|
||||
|
||||
85
server/ctrl/admin.go
Normal file
85
server/ctrl/admin.go
Normal file
@ -0,0 +1,85 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func AdminSessionGet(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
if admin := Config.Get("auth.admin").String(); admin == "" {
|
||||
SendSuccessResult(res, true)
|
||||
return
|
||||
}
|
||||
obfuscate := func() string{
|
||||
c, err := req.Cookie(COOKIE_NAME_ADMIN)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return c.Value
|
||||
}()
|
||||
|
||||
str, err := DecryptString(SECRET_KEY, obfuscate);
|
||||
if err != nil {
|
||||
SendSuccessResult(res, false)
|
||||
return
|
||||
}
|
||||
token := AdminToken{}
|
||||
json.Unmarshal([]byte(str), &token)
|
||||
|
||||
if token.IsAdmin() == false || token.IsValid() == false {
|
||||
SendSuccessResult(res, false)
|
||||
return
|
||||
}
|
||||
SendSuccessResult(res, true)
|
||||
}
|
||||
|
||||
func AdminSessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
// Step 1: Deliberatly make the request slower to make hacking attempt harder for the attacker
|
||||
time.Sleep(1500*time.Millisecond)
|
||||
|
||||
// Step 2: Make sure current user has appropriate access
|
||||
admin := Config.Get("auth.admin").String()
|
||||
if admin == "" {
|
||||
SendErrorResult(res, NewError("Missing admin account, please contact your administrator", 500))
|
||||
return
|
||||
}
|
||||
var params map[string]string
|
||||
b, _ := ioutil.ReadAll(req.Body)
|
||||
json.Unmarshal(b, ¶ms)
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(admin), []byte(params["password"])); err != nil {
|
||||
SendErrorResult(res, ErrInvalidPassword)
|
||||
return
|
||||
}
|
||||
|
||||
// Step 3: Send response to the client
|
||||
body, _ := json.Marshal(NewAdminToken())
|
||||
obfuscate, err := EncryptString(SECRET_KEY, string(body))
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
http.SetCookie(res, &http.Cookie{
|
||||
Name: COOKIE_NAME_ADMIN,
|
||||
Value: obfuscate,
|
||||
Path: COOKIE_PATH_ADMIN,
|
||||
MaxAge: 60*60, // valid for 1 hour
|
||||
})
|
||||
SendSuccessResult(res, true)
|
||||
}
|
||||
|
||||
|
||||
func AdminBackend(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
backends := make(map[string]Form)
|
||||
|
||||
drivers := Backend.Drivers()
|
||||
for key := range drivers {
|
||||
if obj, ok := drivers[key].(interface{ LoginForm() Form }); ok {
|
||||
backends[key] = obj.LoginForm()
|
||||
}
|
||||
}
|
||||
SendSuccessResult(res, backends)
|
||||
}
|
||||
@ -2,15 +2,92 @@ package ctrl
|
||||
|
||||
import (
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
c, err := ctx.Config.Export()
|
||||
var (
|
||||
logpath = filepath.Join(GetCurrentDir(), LOG_PATH, "access.log")
|
||||
cachepath = filepath.Join(GetCurrentDir(), CONFIG_PATH, "config.json")
|
||||
)
|
||||
|
||||
|
||||
func FetchPluginsHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
f, err := os.OpenFile(filepath.Join(GetCurrentDir(), PLUGIN_PATH), os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
res.Write([]byte("window.CONFIG = {}"))
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
res.Write([]byte("window.CONFIG = "))
|
||||
res.Write([]byte(c))
|
||||
files, err := f.Readdir(0)
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
plugins := make([]string, 0)
|
||||
for i := 0; i < len(files); i++ {
|
||||
plugins = append(plugins, files[i].Name())
|
||||
}
|
||||
SendSuccessResults(res, plugins)
|
||||
}
|
||||
|
||||
func FetchLogHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
file, err := os.OpenFile(logpath, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
maxSize := req.URL.Query().Get("maxSize")
|
||||
if maxSize != "" {
|
||||
cursor := func() int64 {
|
||||
tmp, err := strconv.Atoi(maxSize)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int64(tmp)
|
||||
}()
|
||||
for cursor >= 0 {
|
||||
if _, err := file.Seek(-cursor, io.SeekEnd); err != nil {
|
||||
break
|
||||
}
|
||||
char := make([]byte, 1)
|
||||
file.Read(char)
|
||||
if char[0] == 10 || char[0] == 13 { // stop if we find a line
|
||||
break
|
||||
}
|
||||
cursor += 1
|
||||
}
|
||||
}
|
||||
res.Header().Set("Content-Type", "text/plain")
|
||||
io.Copy(res, file)
|
||||
}
|
||||
|
||||
func PrivateConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
SendSuccessResult(res, Config)
|
||||
}
|
||||
|
||||
func PrivateConfigUpdateHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
b, _ := ioutil.ReadAll(req.Body)
|
||||
b = PrettyPrint(b)
|
||||
file, err := os.Create(cachepath)
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := file.Write(b); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
file.Close()
|
||||
Config.Load()
|
||||
SendSuccessResult(res, nil)
|
||||
}
|
||||
|
||||
func PublicConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
SendSuccessResult(res, Config.Export())
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
SendErrorResult(res, NewError(err.Error(), 500))
|
||||
return
|
||||
}
|
||||
obfuscate, err := EncryptString(ctx.Config.Get("general.secret_key").String(), string(s))
|
||||
obfuscate, err := EncryptString(SECRET_KEY, string(s))
|
||||
if err != nil {
|
||||
SendErrorResult(res, NewError(err.Error(), 500))
|
||||
return
|
||||
@ -93,19 +93,23 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func SessionLogout(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
cookie := http.Cookie{
|
||||
Name: COOKIE_NAME_AUTH,
|
||||
Value: "",
|
||||
Path: COOKIE_PATH,
|
||||
MaxAge: -1,
|
||||
}
|
||||
if ctx.Backend != nil {
|
||||
if obj, ok := ctx.Backend.(interface{ Close() error }); ok {
|
||||
go obj.Close()
|
||||
}
|
||||
}
|
||||
|
||||
http.SetCookie(res, &cookie)
|
||||
http.SetCookie(res, &http.Cookie{
|
||||
Name: COOKIE_NAME_AUTH,
|
||||
Value: "",
|
||||
Path: COOKIE_PATH,
|
||||
MaxAge: -1,
|
||||
})
|
||||
http.SetCookie(res, &http.Cookie{
|
||||
Name: COOKIE_NAME_ADMIN,
|
||||
Value: "",
|
||||
Path: COOKIE_PATH_ADMIN,
|
||||
MaxAge: -1,
|
||||
})
|
||||
SendSuccessResult(res, nil)
|
||||
}
|
||||
|
||||
|
||||
@ -178,7 +178,7 @@ func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if submittedProof.Key != "<nil>" {
|
||||
if submittedProof.Key != "" {
|
||||
submittedProof.Id = Hash(submittedProof.Key + "::" + submittedProof.Value)
|
||||
verifiedProof = append(verifiedProof, submittedProof)
|
||||
}
|
||||
|
||||
@ -39,15 +39,17 @@ func StaticHandler(_path string, ctx App) http.Handler {
|
||||
|
||||
func DefaultHandler(_path string, ctx App) http.Handler {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" {
|
||||
http.Error(res, "Invalid request method.", 405)
|
||||
return
|
||||
}
|
||||
|
||||
header := res.Header()
|
||||
header.Set("Content-Type", "text/html")
|
||||
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
SecureHeader(&header)
|
||||
|
||||
// Redirect to the admin section on first boot to setup the stuff
|
||||
if req.URL.String() != URL_SETUP && Config.Get("auth.admin").String() == "" {
|
||||
http.Redirect(res, req, URL_SETUP, 307)
|
||||
return
|
||||
}
|
||||
|
||||
p := _path
|
||||
if _, err := os.Open(path.Join(GetCurrentDir(), p+".gz")); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||
res.Header().Set("Content-Encoding", "gzip")
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
. "github.com/mickael-kerjean/nuage/server/middleware"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"github.com/mickael-kerjean/net/webdav"
|
||||
"github.com/mickael-kerjean/mux"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -17,14 +19,28 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
prefix := "/s/" + share_id
|
||||
req.Header.Del("Content-Type")
|
||||
|
||||
if req.Method == "GET" {
|
||||
if req.URL.Path == prefix {
|
||||
DefaultHandler(FILE_INDEX, ctx).ServeHTTP(res, req)
|
||||
return
|
||||
if req.Method == "GET" && req.URL.Path == prefix {
|
||||
DefaultHandler(FILE_INDEX, ctx).ServeHTTP(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
isCrap := func(p string) bool {
|
||||
if strings.HasPrefix(p, ".") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}(filepath.Base(req.URL.Path))
|
||||
if isCrap == true {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if ctx.Share, err = ExtractShare(req, &ctx, share_id); err != nil {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Session, err = ExtractSession(req, &ctx); err != nil {
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
@ -38,22 +54,21 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// webdav is WIP
|
||||
http.NotFound(res, req)
|
||||
return
|
||||
|
||||
Log.Warning("==== REQUEST ('%s'): %s", req.Method, req.URL.Path)
|
||||
//start := time.Now()
|
||||
h := &webdav.Handler{
|
||||
Prefix: "/s/" + share_id,
|
||||
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Session["path"]),
|
||||
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Share.Path),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
Logger: func(r *http.Request, err error) {
|
||||
e := func(err error) string{
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return "OK"
|
||||
}(err)
|
||||
Log.Info("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)
|
||||
//Log.Info("==== REQUEST ('%s' => %d): %s\n", req.Method, time.Now().Sub(start) / (1000 * 1000), req.URL.Path)
|
||||
// e := func(err error) string{
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// return "OK"
|
||||
// }(err)
|
||||
//Log.Info("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)
|
||||
},
|
||||
}
|
||||
h.ServeHTTP(res, req)
|
||||
|
||||
@ -15,8 +15,6 @@ import (
|
||||
|
||||
func main() {
|
||||
app := App{}
|
||||
app.Config = NewConfig()
|
||||
Log.SetVisibility(app.Config.Get("log.level").String())
|
||||
Init(&app)
|
||||
}
|
||||
|
||||
@ -48,36 +46,47 @@ func Init(a *App) {
|
||||
session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET")
|
||||
|
||||
files := r.PathPrefix("/api/files").Subrouter()
|
||||
files.HandleFunc("/ls", APIHandler(LoggedInOnly(FileLs), *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileCat), *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
|
||||
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
|
||||
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
|
||||
files.HandleFunc("/ls", APIHandler(LoggedInOnly(FileLs), *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileCat), *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
|
||||
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
|
||||
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
|
||||
files.HandleFunc("/mkdir", APIHandler(LoggedInOnly(FileMkdir), *a)).Methods("GET")
|
||||
files.HandleFunc("/touch", APIHandler(LoggedInOnly(FileTouch), *a)).Methods("GET")
|
||||
|
||||
share := r.PathPrefix("/api/share").Subrouter()
|
||||
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
|
||||
share.HandleFunc("/{share}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
||||
share.HandleFunc("/{share}", APIHandler(ShareDelete, *a)).Methods("DELETE")
|
||||
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
|
||||
share.HandleFunc("/{share}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
||||
share.HandleFunc("/{share}", APIHandler(ShareDelete, *a)).Methods("DELETE")
|
||||
share.HandleFunc("/{share}/proof", APIHandler(ShareVerifyProof, *a)).Methods("POST")
|
||||
|
||||
// WEBDAV
|
||||
r.PathPrefix("/s/{share}").Handler(CtxInjector(WebdavHandler, *a))
|
||||
|
||||
// ADMIN
|
||||
admin := r.PathPrefix("/admin/api").Subrouter()
|
||||
admin.HandleFunc("/session", CtxInjector(AdminSessionGet, *a)).Methods("GET")
|
||||
admin.HandleFunc("/session", CtxInjector(AdminSessionAuthenticate, *a)).Methods("POST")
|
||||
admin.HandleFunc("/backend", CtxInjector(AdminBackend, *a)).Methods("GET")
|
||||
admin.HandleFunc("/plugin", CtxInjector(AdminOnly(FetchPluginsHandler), *a)).Methods("GET")
|
||||
admin.HandleFunc("/log", CtxInjector(AdminOnly(FetchLogHandler), *a)).Methods("GET")
|
||||
admin.HandleFunc("/config", CtxInjector(AdminOnly(PrivateConfigHandler), *a)).Methods("GET")
|
||||
admin.HandleFunc("/config", CtxInjector(AdminOnly(PrivateConfigUpdateHandler), *a)).Methods("POST")
|
||||
|
||||
// APP
|
||||
r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET")
|
||||
r.HandleFunc("/api/config", CtxInjector(PublicConfigHandler, *a)).Methods("GET")
|
||||
r.PathPrefix("/assets").Handler(StaticHandler(FILE_ASSETS, *a)).Methods("GET")
|
||||
r.PathPrefix("/about").Handler(AboutHandler(*a))
|
||||
r.PathPrefix("/about").Handler(AboutHandler(*a)).Methods("GET")
|
||||
r.PathPrefix("/").Handler(DefaultHandler(FILE_INDEX, *a)).Methods("GET")
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + strconv.Itoa(a.Config.Get("general.port").Int()),
|
||||
Addr: ":" + strconv.Itoa(Config.Get("general.port").Int()),
|
||||
Handler: r,
|
||||
}
|
||||
Log.Info("STARTING SERVER")
|
||||
|
||||
Log.Stdout("STARTING SERVER")
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
Log.Error("Server start: %v", err)
|
||||
Log.Stdout("Server start: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
45
server/middleware/admin.go
Normal file
45
server/middleware/admin.go
Normal file
@ -0,0 +1,45 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func LoggedInOnly(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
if ctx.Backend == nil || ctx.Session == nil {
|
||||
SendErrorResult(res, NewError("Forbidden", 403))
|
||||
return
|
||||
}
|
||||
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func AdminOnly(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
if admin := Config.Get("auth.admin").String(); admin != "" {
|
||||
c, err := req.Cookie(COOKIE_NAME_ADMIN);
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
|
||||
str, err := DecryptString(SECRET_KEY, c.Value);
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
token := AdminToken{}
|
||||
json.Unmarshal([]byte(str), &token)
|
||||
|
||||
if token.IsValid() == false || token.IsAdmin() == false {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
12
server/middleware/context.go
Normal file
12
server/middleware/context.go
Normal file
@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func CtxInjector(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
fn(ctx, res, req)
|
||||
})
|
||||
}
|
||||
@ -16,7 +16,8 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
|
||||
var err error
|
||||
start := time.Now()
|
||||
|
||||
res.Header().Add("Content-Type", "application/json")
|
||||
header := res.Header()
|
||||
header.Add("Content-Type", "application/json")
|
||||
if ctx.Body, err = ExtractBody(req); err != nil {
|
||||
SendErrorResult(res, ErrNotValid)
|
||||
return
|
||||
@ -39,32 +40,16 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
|
||||
req.Body.Close()
|
||||
|
||||
go func() {
|
||||
if ctx.Config.Get("log.telemetry").Bool() {
|
||||
if Config.Get("log.telemetry").Bool() {
|
||||
go telemetry(req, &resw, start, ctx.Backend.Info())
|
||||
}
|
||||
if ctx.Config.Get("log.enable").Bool() {
|
||||
if Config.Get("log.enable").Bool() {
|
||||
go logger(req, &resw, start)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func LoggedInOnly(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
if ctx.Backend == nil || ctx.Session == nil {
|
||||
SendErrorResult(res, NewError("Forbidden", 403))
|
||||
return
|
||||
}
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func CtxInjector(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
fn(ctx, res, req)
|
||||
})
|
||||
}
|
||||
|
||||
func ExtractBody(req *http.Request) (map[string]interface{}, error) {
|
||||
var body map[string]interface{}
|
||||
|
||||
@ -90,7 +75,8 @@ func ExtractShare(req *http.Request, ctx *App, share_id string) (Share, error) {
|
||||
return Share{}, nil
|
||||
}
|
||||
|
||||
if ctx.Config.Get("features.share.enable").Bool() == false {
|
||||
if Config.Get("features.share.enable").Bool() == false {
|
||||
Log.Debug("Share feature isn't enable, contact your administrator")
|
||||
return Share{}, NewError("Feature isn't enable, contact your administrator", 405)
|
||||
}
|
||||
|
||||
|
||||
@ -28,9 +28,9 @@ func (d Dropbox) Init(params map[string]string, app *App) (IBackend, error) {
|
||||
if env := os.Getenv("DROPBOX_CLIENT_ID"); env != "" {
|
||||
backend.ClientId = env
|
||||
} else {
|
||||
backend.ClientId = app.Config.Get("oauth.dropbox.client_id").Default("").String()
|
||||
backend.ClientId = Config.Get("auth.dropbox.client_id").Default("").String()
|
||||
}
|
||||
backend.Hostname = app.Config.Get("general.host").String()
|
||||
backend.Hostname = Config.Get("general.host").String()
|
||||
backend.Bearer = params["bearer"]
|
||||
|
||||
if backend.ClientId == "" {
|
||||
@ -45,6 +45,23 @@ func (d Dropbox) Info() string {
|
||||
return "dropbox"
|
||||
}
|
||||
|
||||
func (d Dropbox) LoginForm() Form {
|
||||
return Form{
|
||||
Elmnts: []FormElement{
|
||||
FormElement{
|
||||
Name: "type",
|
||||
Type: "hidden",
|
||||
Value: "dropbox",
|
||||
},
|
||||
FormElement{
|
||||
Name: "image",
|
||||
Type: "image",
|
||||
Value: "/assets/img/dropbox.png",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d Dropbox) OAuthURL() string {
|
||||
url := "https://www.dropbox.com/oauth2/authorize?"
|
||||
url += "client_id=" + d.ClientId
|
||||
|
||||
@ -74,6 +74,57 @@ func (f Ftp) Info() string {
|
||||
return "ftp"
|
||||
}
|
||||
|
||||
func (f Ftp) LoginForm() Form {
|
||||
return Form{
|
||||
Elmnts: []FormElement{
|
||||
FormElement{
|
||||
Name: "type",
|
||||
Type: "hidden",
|
||||
Value: "ftp",
|
||||
},
|
||||
FormElement{
|
||||
Name: "hostname",
|
||||
Type: "text",
|
||||
Placeholder: "Hostname*",
|
||||
},
|
||||
FormElement{
|
||||
Name: "username",
|
||||
Type: "text",
|
||||
Placeholder: "Username",
|
||||
},
|
||||
FormElement{
|
||||
Name: "password",
|
||||
Type: "password",
|
||||
Placeholder: "Password",
|
||||
},
|
||||
FormElement{
|
||||
Name: "advanced",
|
||||
Type: "enable",
|
||||
Placeholder: "Advanced",
|
||||
Target: []string{"ftp_path", "ftp_port", "ftp_conn"},
|
||||
},
|
||||
FormElement{
|
||||
Id: "ftp_path",
|
||||
Name: "path",
|
||||
Type: "text",
|
||||
Placeholder: "Path",
|
||||
},
|
||||
FormElement{
|
||||
Id: "ftp_port",
|
||||
Name: "port",
|
||||
Type: "number",
|
||||
Placeholder: "Port",
|
||||
},
|
||||
FormElement{
|
||||
Id: "ftp_conn",
|
||||
Name: "conn",
|
||||
Type: "number",
|
||||
Placeholder: "Number of connections",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f Ftp) Home() (string, error) {
|
||||
return f.client.Getwd()
|
||||
}
|
||||
|
||||
@ -32,9 +32,9 @@ func (g GDrive) Init(params map[string]string, app *App) (IBackend, error) {
|
||||
|
||||
config := &oauth2.Config{
|
||||
Endpoint: google.Endpoint,
|
||||
ClientID: app.Config.Get("oauth.gdrive.client_id").Default(os.Getenv("GDRIVE_CLIENT_ID")).String(),
|
||||
ClientSecret: app.Config.Get("oauth.gdrive.client_secret").Default(os.Getenv("GDRIVE_CLIENT_SECRET")).String(),
|
||||
RedirectURL: app.Config.Get("general.host").String() + "/login",
|
||||
ClientID: Config.Get("auth.gdrive.client_id").Default(os.Getenv("GDRIVE_CLIENT_ID")).String(),
|
||||
ClientSecret: Config.Get("auth.gdrive.client_secret").Default(os.Getenv("GDRIVE_CLIENT_SECRET")).String(),
|
||||
RedirectURL: Config.Get("general.host").String() + "/login",
|
||||
Scopes: []string{"https://www.googleapis.com/auth/drive"},
|
||||
}
|
||||
if config.ClientID == "" {
|
||||
@ -71,6 +71,23 @@ func (g GDrive) Info() string {
|
||||
return "googledrive"
|
||||
}
|
||||
|
||||
func (g GDrive) LoginForm() Form {
|
||||
return Form{
|
||||
Elmnts: []FormElement{
|
||||
FormElement{
|
||||
Name: "type",
|
||||
Type: "hidden",
|
||||
Value: "gdrive",
|
||||
},
|
||||
FormElement{
|
||||
Name: "image",
|
||||
Type: "image",
|
||||
Value: "/assets/img/google-drive.png",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (g GDrive) OAuthURL() string {
|
||||
return g.Config.AuthCodeURL("googledrive", oauth2.AccessTypeOnline)
|
||||
}
|
||||
|
||||
@ -112,6 +112,93 @@ func (g Git) Info() string {
|
||||
return "git"
|
||||
}
|
||||
|
||||
func (g Git) LoginForm() Form {
|
||||
return Form{
|
||||
Elmnts: []FormElement{
|
||||
FormElement{
|
||||
Name: "type",
|
||||
Value: "git",
|
||||
Type: "hidden",
|
||||
},
|
||||
FormElement{
|
||||
Name: "repo",
|
||||
Type: "text",
|
||||
Placeholder: "Repository*",
|
||||
},
|
||||
FormElement{
|
||||
Name: "username",
|
||||
Type: "text",
|
||||
Placeholder: "Username",
|
||||
},
|
||||
FormElement{
|
||||
Name: "password",
|
||||
Type: "password",
|
||||
Placeholder: "Password",
|
||||
},
|
||||
FormElement{
|
||||
Name: "advanced",
|
||||
Type: "enable",
|
||||
Placeholder: "Advanced",
|
||||
Target: []string{
|
||||
"git_path", "git_passphrase", "git_commit",
|
||||
"git_branch", "git_author_email", "git_author_name",
|
||||
"git_committer_email", "git_committer_name",
|
||||
},
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_path",
|
||||
Name: "path",
|
||||
Type: "text",
|
||||
Placeholder: "Path",
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_passphrase",
|
||||
Name: "passphrase",
|
||||
Type: "text",
|
||||
Placeholder: "Passphrase",
|
||||
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_commit",
|
||||
Name: "commit",
|
||||
Type: "text",
|
||||
Placeholder: "Commit Format: default to \"{action}({filename}): {path}\"",
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_branch",
|
||||
Name: "branch",
|
||||
Type: "text",
|
||||
Placeholder: "Branch: default to \"master\"",
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_author_email",
|
||||
Name: "author_email",
|
||||
Type: "text",
|
||||
Placeholder: "Author email",
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_author_name",
|
||||
Name: "author_name",
|
||||
Type: "text",
|
||||
Placeholder: "Author name",
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_committer_email",
|
||||
Name: "committer_email",
|
||||
Type: "text",
|
||||
Placeholder: "Committer email",
|
||||
},
|
||||
FormElement{
|
||||
Id: "git_committer_name",
|
||||
Name: "committer_name",
|
||||
Type: "text",
|
||||
Placeholder: "Committer name",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (g Git) Ls(path string) ([]os.FileInfo, error) {
|
||||
g.git.refresh()
|
||||
p, err := g.path(path)
|
||||
|
||||
@ -57,6 +57,58 @@ func (s S3Backend) Info() string {
|
||||
return "s3"
|
||||
}
|
||||
|
||||
func (s S3Backend) LoginForm() Form {
|
||||
return Form{
|
||||
Elmnts: []FormElement{
|
||||
FormElement{
|
||||
Name: "type",
|
||||
Type: "hidden",
|
||||
Value: "s3",
|
||||
},
|
||||
FormElement{
|
||||
Name: "access_key_id",
|
||||
Type: "text",
|
||||
Placeholder: "Access Key ID*",
|
||||
},
|
||||
FormElement{
|
||||
Name: "secret_access_key",
|
||||
Type: "text",
|
||||
Placeholder: "Secret Access Key*",
|
||||
},
|
||||
FormElement{
|
||||
Name: "advanced",
|
||||
Type: "enable",
|
||||
Placeholder: "Advanced",
|
||||
Target: []string{"s3_path", "s3_encryption_key", "s3_region", "s3_endpoint"},
|
||||
},
|
||||
FormElement{
|
||||
Id: "s3_path",
|
||||
Name: "path",
|
||||
Type: "text",
|
||||
Placeholder: "Path",
|
||||
},
|
||||
FormElement{
|
||||
Id: "s3_encryption_key",
|
||||
Name: "encryption_key",
|
||||
Type: "text",
|
||||
Placeholder: "Encryption Key",
|
||||
},
|
||||
FormElement{
|
||||
Id: "s3_region",
|
||||
Name: "region",
|
||||
Type: "text",
|
||||
Placeholder: "Region",
|
||||
},
|
||||
FormElement{
|
||||
Id: "s3_endpoint",
|
||||
Name: "endpoint",
|
||||
Type: "text",
|
||||
Placeholder: "Endpoint",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s S3Backend) Meta(path string) Metadata {
|
||||
if path == "/" {
|
||||
return Metadata{
|
||||
|
||||
@ -95,6 +95,58 @@ func (b Sftp) Info() string {
|
||||
return "sftp"
|
||||
}
|
||||
|
||||
func (b Sftp) LoginForm() Form {
|
||||
return Form{
|
||||
Elmnts: []FormElement{
|
||||
FormElement{
|
||||
Name: "type",
|
||||
Type: "hidden",
|
||||
Value: "sftp",
|
||||
},
|
||||
FormElement{
|
||||
Name: "hostname",
|
||||
Type: "text",
|
||||
Placeholder: "Hostname*",
|
||||
},
|
||||
FormElement{
|
||||
Name: "username",
|
||||
Type: "text",
|
||||
Placeholder: "Username",
|
||||
},
|
||||
FormElement{
|
||||
Name: "password",
|
||||
Type: "password",
|
||||
Placeholder: "Password",
|
||||
},
|
||||
FormElement{
|
||||
Name: "advanced",
|
||||
Type: "enable",
|
||||
Placeholder: "Advanced",
|
||||
Target: []string{"sftp_path", "sftp_port", "sftp_passphrase"},
|
||||
},
|
||||
FormElement{
|
||||
Id: "sftp_path",
|
||||
Name: "path",
|
||||
Type: "text",
|
||||
Placeholder: "Path",
|
||||
},
|
||||
FormElement{
|
||||
Id: "sftp_port",
|
||||
Name: "port",
|
||||
Type: "number",
|
||||
Placeholder: "Port",
|
||||
|
||||
},
|
||||
FormElement{
|
||||
Id: "sftp_passphrase",
|
||||
Name: "passphrase",
|
||||
Type: "text",
|
||||
Placeholder: "Passphrase",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b Sftp) Home() (string, error) {
|
||||
cwd, err := b.SFTPClient.Getwd()
|
||||
if err != nil {
|
||||
@ -113,7 +165,7 @@ func (b Sftp) Ls(path string) ([]os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (b Sftp) Cat(path string) (io.Reader, error) {
|
||||
remoteFile, err := b.SFTPClient.Open(path)
|
||||
remoteFile, err := b.SFTPClient.OpenFile(path, os.O_RDONLY)
|
||||
if err != nil {
|
||||
return nil, b.err(err)
|
||||
}
|
||||
|
||||
@ -45,6 +45,45 @@ func (w WebDav) Info() string {
|
||||
return "webdav"
|
||||
}
|
||||
|
||||
func (w WebDav) LoginForm() Form {
|
||||
return Form{
|
||||
Elmnts: []FormElement{
|
||||
FormElement{
|
||||
Name: "type",
|
||||
Type: "hidden",
|
||||
Value: "webdav",
|
||||
},
|
||||
FormElement{
|
||||
Name: "url",
|
||||
Type: "text",
|
||||
Placeholder: "Address*",
|
||||
},
|
||||
FormElement{
|
||||
Name: "username",
|
||||
Type: "text",
|
||||
Placeholder: "Username",
|
||||
},
|
||||
FormElement{
|
||||
Name: "password",
|
||||
Type: "password",
|
||||
Placeholder: "Password",
|
||||
},
|
||||
FormElement{
|
||||
Name: "advanced",
|
||||
Type: "enable",
|
||||
Placeholder: "Advanced",
|
||||
Target: []string{"webdav_path"},
|
||||
},
|
||||
FormElement{
|
||||
Id: "webdav_path",
|
||||
Name: "path",
|
||||
Type: "text",
|
||||
Placeholder: "Path",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (w WebDav) Ls(path string) ([]os.FileInfo, error) {
|
||||
files := make([]os.FileInfo, 0)
|
||||
query := `<d:propfind xmlns:d='DAV:'>
|
||||
|
||||
@ -9,26 +9,28 @@ import (
|
||||
|
||||
func NewBackend(ctx *App, conn map[string]string) (IBackend, error) {
|
||||
isAllowed := func() bool {
|
||||
ret := false
|
||||
var conns [] struct {
|
||||
Type string `json:"type"`
|
||||
Hostname string `json:"hostname"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
ctx.Config.Get("connections").Scan(&conns)
|
||||
for i := range conns {
|
||||
if conns[i].Type == conn["type"] {
|
||||
if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] {
|
||||
continue
|
||||
} else if conns[i].Path != "" && conns[i].Path != conn["path"] {
|
||||
continue
|
||||
} else {
|
||||
ret = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
return true
|
||||
// ret := false
|
||||
// var conns [] struct {
|
||||
// Type string `json:"type"`
|
||||
// Hostname string `json:"hostname"`
|
||||
// Path string `json:"path"`
|
||||
// }
|
||||
// Config.Get("connections").Interface()
|
||||
// Config.Get("connections").Scan(&conns)
|
||||
// for i := range conns {
|
||||
// if conns[i].Type == conn["type"] {
|
||||
// if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] {
|
||||
// continue
|
||||
// } else if conns[i].Path != "" && conns[i].Path != conn["path"] {
|
||||
// continue
|
||||
// } else {
|
||||
// ret = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return ret
|
||||
}()
|
||||
|
||||
if isAllowed == false {
|
||||
@ -57,6 +59,9 @@ func MapStringInterfaceToMapStringString(m map[string]interface{}) map[string]st
|
||||
res := make(map[string]string)
|
||||
for key, value := range m {
|
||||
res[key] = fmt.Sprintf("%v", value)
|
||||
if res[key] == "<nil>" {
|
||||
res[key] = ""
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
|
||||
}
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(*s.Password), []byte(proof.Value)); err != nil {
|
||||
return p, NewError("Invalid Password", 403)
|
||||
return p, ErrInvalidPassword
|
||||
}
|
||||
p.Value = *s.Password
|
||||
}
|
||||
@ -188,16 +188,18 @@ func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
|
||||
p.Message = NewString("We've sent you a message with a verification code")
|
||||
|
||||
// Send email
|
||||
var email struct {
|
||||
email := struct {
|
||||
Hostname string `json:"server"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
From string `json:"from"`
|
||||
}
|
||||
if err := ctx.Config.Get("email").Scan(&email); err != nil {
|
||||
Log.Error("ERROR(%+v)", err)
|
||||
return p, nil
|
||||
}{
|
||||
Hostname: Config.Get("email.server").String(),
|
||||
Port: Config.Get("email.port").Int(),
|
||||
Username: Config.Get("email.username").String(),
|
||||
Password: Config.Get("email.password").String(),
|
||||
From: Config.Get("email.from").String(),
|
||||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const DAVCachePath = "data/cache/webdav/"
|
||||
@ -23,7 +22,6 @@ func init() {
|
||||
type WebdavFs struct {
|
||||
backend IBackend
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewWebdavFs(b IBackend, path string) WebdavFs {
|
||||
@ -34,6 +32,7 @@ func NewWebdavFs(b IBackend, path string) WebdavFs {
|
||||
}
|
||||
|
||||
func (fs WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||
Log.Info("MKDIR ('%s')", name)
|
||||
if name = fs.resolve(name); name == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@ -41,10 +40,12 @@ func (fs WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) err
|
||||
}
|
||||
|
||||
func (fs WebdavFs) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
Log.Info("OPEN_FILE ('%s')", name)
|
||||
return NewWebdavNode(name, fs), nil
|
||||
}
|
||||
|
||||
func (fs WebdavFs) RemoveAll(ctx context.Context, name string) error {
|
||||
Log.Info("RM ('%s')", name)
|
||||
if name = fs.resolve(name); name == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@ -52,6 +53,7 @@ func (fs WebdavFs) RemoveAll(ctx context.Context, name string) error {
|
||||
}
|
||||
|
||||
func (fs WebdavFs) Rename(ctx context.Context, oldName, newName string) error {
|
||||
Log.Info("MV ('%s' => '%s')", oldName, newName)
|
||||
if oldName = fs.resolve(oldName); oldName == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@ -62,6 +64,7 @@ func (fs WebdavFs) Rename(ctx context.Context, oldName, newName string) error {
|
||||
}
|
||||
|
||||
func (fs WebdavFs) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
Log.Info("STAT ('%s')", name)
|
||||
if name = fs.resolve(name); name == "" {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
@ -99,6 +102,7 @@ func NewWebdavNode(name string, fs WebdavFs) *WebdavNode {
|
||||
}
|
||||
|
||||
func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
|
||||
Log.Info(" => READ_DIR ('%s')", w.path)
|
||||
var path string
|
||||
if path = w.fs.resolve(w.path); path == "" {
|
||||
return nil, os.ErrInvalid
|
||||
@ -107,27 +111,29 @@ func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (w *WebdavNode) Stat() (os.FileInfo, error) {
|
||||
if w.filewrite != nil {
|
||||
var path string
|
||||
var err error
|
||||
Log.Info(" => STAT ('%s')", w.path)
|
||||
// if w.filewrite != nil {
|
||||
// var path stringc
|
||||
// var err error
|
||||
|
||||
if path = w.fs.resolve(w.path); path == "" {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
name := w.filewrite.Name()
|
||||
w.filewrite.Close()
|
||||
if w.filewrite, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm); err != nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
// if path = w.fs.resolve(w.path); path == "" {
|
||||
// return nil, os.ErrInvalid
|
||||
// }
|
||||
// name := w.filewrite.Name()
|
||||
// w.filewrite.Close()
|
||||
// if w.filewrite, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm); err != nil {
|
||||
// return nil, os.ErrInvalid
|
||||
// }
|
||||
|
||||
if err = w.fs.backend.Save(path, w.filewrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// if err = w.fs.backend.Save(path, w.filewrite); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// }
|
||||
return w.fs.Stat(context.Background(), w.path)
|
||||
}
|
||||
|
||||
func (w *WebdavNode) Close() error {
|
||||
Log.Info(" => CLOSE ('%s')", w.path)
|
||||
if w.fileread != nil {
|
||||
if err := w.cleanup(w.fileread); err != nil {
|
||||
return err
|
||||
@ -135,15 +141,27 @@ func (w *WebdavNode) Close() error {
|
||||
w.fileread = nil
|
||||
}
|
||||
if w.filewrite != nil {
|
||||
if err := w.cleanup(w.filewrite); err != nil {
|
||||
defer w.cleanup(w.filewrite)
|
||||
name := w.filewrite.Name()
|
||||
w.filewrite.Close()
|
||||
reader, err := os.OpenFile(name, os.O_RDONLY, os.ModePerm);
|
||||
if err != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
path := w.fs.resolve(w.path)
|
||||
if path == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if err := w.fs.backend.Save(path, reader); err != nil {
|
||||
return err
|
||||
}
|
||||
w.filewrite = nil
|
||||
reader.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WebdavNode) Read(p []byte) (int, error) {
|
||||
Log.Info(" => READ ('%s')", w.path)
|
||||
if w.fileread != nil {
|
||||
return w.fileread.Read(p)
|
||||
}
|
||||
@ -151,30 +169,36 @@ func (w *WebdavNode) Read(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (w *WebdavNode) Seek(offset int64, whence int) (int64, error) {
|
||||
Log.Info(" => SEEK ('%s')", w.path)
|
||||
var path string
|
||||
var err error
|
||||
if path = w.fs.resolve(w.path); path == "" {
|
||||
return -1, os.ErrInvalid
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
|
||||
if w.fileread == nil {
|
||||
var reader io.Reader
|
||||
if w.fileread, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_RDWR|os.O_CREATE, os.ModePerm); err != nil {
|
||||
if w.fileread, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err != nil {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
if reader, err = w.fs.backend.Cat(path); err != nil {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
io.Copy(w.fileread, reader)
|
||||
|
||||
name := w.fileread.Name()
|
||||
w.fileread.Close()
|
||||
w.fileread, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm)
|
||||
}
|
||||
return w.fileread.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (w *WebdavNode) Write(p []byte) (int, error) {
|
||||
Log.Info(" => WRITE ('%s')", w.path)
|
||||
var err error
|
||||
|
||||
if w.filewrite == nil {
|
||||
if w.filewrite, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_RDWR|os.O_CREATE, os.ModePerm); err != nil {
|
||||
if w.filewrite, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err != nil {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
}
|
||||
@ -183,11 +207,7 @@ func (w *WebdavNode) Write(p []byte) (int, error) {
|
||||
|
||||
func (w *WebdavNode) cleanup(file *os.File) error {
|
||||
name := file.Name()
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(name); err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close();
|
||||
os.Remove(name);
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -18,7 +18,6 @@ func init() {
|
||||
}
|
||||
files, err := file.Readdir(0)
|
||||
|
||||
c := NewConfig()
|
||||
for i:=0; i < len(files); i++ {
|
||||
name := files[i].Name()
|
||||
if strings.HasPrefix(name, ".") {
|
||||
@ -34,8 +33,8 @@ func init() {
|
||||
Log.Warning("Can't register plugin: %s => %v", name, err)
|
||||
continue
|
||||
}
|
||||
if obj, ok := fn.(func(config *Config)); ok {
|
||||
obj(c)
|
||||
if obj, ok := fn.(func(config *Configuration)); ok {
|
||||
obj(&Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user