mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 16:32:31 +01:00
feature (admin): admin console
This commit is contained in:
parent
1b591af5b3
commit
ce6a228968
78 changed files with 2699 additions and 386 deletions
|
|
@ -83,6 +83,7 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,8 +101,7 @@ button::-moz-focus-inner {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input, textarea, select {
|
||||||
textarea {
|
|
||||||
transition: border 0.2s;
|
transition: border 0.2s;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
@ -109,9 +109,9 @@ textarea {
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-select {
|
.no-select {
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,16 @@ export class ModalAlert extends Popup {
|
||||||
alert.subscribe((Component, okCallback) => {
|
alert.subscribe((Component, okCallback) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
appear: true,
|
appear: true,
|
||||||
value: Component
|
value: Component,
|
||||||
|
fn: okCallback
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(e){
|
onSubmit(e){
|
||||||
this.setState({appear: false});
|
this.setState({appear: false}, () => {
|
||||||
|
requestAnimationFrame(() => this.state.fn())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
modalContentBody(){
|
modalContentBody(){
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ button{
|
||||||
background: var(--emphasis);
|
background: var(--emphasis);
|
||||||
color: white
|
color: white
|
||||||
}
|
}
|
||||||
&.transparent{
|
&.dark{
|
||||||
|
background: var(--dark);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import './container.scss';
|
import './container.scss';
|
||||||
|
|
||||||
export class Container extends React.Component {
|
export class Container extends React.Component {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
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 { Container, Loader, Icon } from '../components/';
|
||||||
import { memory, currentShare } from '../helpers/';
|
import { memory, currentShare } from '../helpers/';
|
||||||
|
|
||||||
|
|
@ -47,7 +47,6 @@ export function LoggedInOnly(WrappedComponent){
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function ErrorPage(WrappedComponent){
|
export function ErrorPage(WrappedComponent){
|
||||||
return class extends React.Component {
|
return class extends React.Component {
|
||||||
constructor(props){
|
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 { EventEmitter, EventReceiver } from './events';
|
||||||
export { BreadCrumb, PathElement } from './breadcrumb';
|
export { BreadCrumb, PathElement } from './breadcrumb';
|
||||||
export { Input } from './input';
|
export { Input, Select, Enabler } from './input';
|
||||||
export { Textarea } from './textarea';
|
export { Textarea } from './textarea';
|
||||||
export { Button } from './button';
|
export { Button } from './button';
|
||||||
export { Container } from './container';
|
export { Container } from './container';
|
||||||
|
|
@ -19,7 +19,5 @@ export { Audio } from './audio';
|
||||||
export { Video } from './video';
|
export { Video } from './video';
|
||||||
export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown';
|
export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown';
|
||||||
export { MapShot } from './mapshot';
|
export { MapShot } from './mapshot';
|
||||||
export { LoggedInOnly, ErrorPage } from './decorator';
|
export { LoggedInOnly, ErrorPage, LoadingPage } from './decorator';
|
||||||
//export { Connect } from './connect';
|
export { FormBuilder } from './formbuilder';
|
||||||
// Those are commented because they will delivered as a separate chunk
|
|
||||||
// export { Editor } from './editor';
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ export class Input extends React.Component {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
className="component_input"
|
className="component_input"
|
||||||
|
onChange={this.props.onChange}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
ref={(comp) => { this.ref = comp; }}
|
ref={(comp) => { this.ref = comp; }} />
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,3 +22,67 @@ Input.propTypes = {
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
placeholder: 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);
|
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'){
|
if(type === 'json'){
|
||||||
try{
|
try{
|
||||||
let data = JSON.parse(xhr.responseText);
|
let data = JSON.parse(xhr.responseText);
|
||||||
if(data.status === 'ok'){
|
if("status" in data === false || data.status === 'ok'){
|
||||||
done(data);
|
done(data);
|
||||||
}else{
|
}else{
|
||||||
err(data);
|
err(data);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
const algorithm = 'aes-256-cbc';
|
const algorithm = 'aes-256-cbc';
|
||||||
|
|
||||||
export function encrypt(obj, key){
|
export function encrypt(obj, key){
|
||||||
|
|
@ -11,3 +13,12 @@ export function decrypt(text, key){
|
||||||
var decipher = crypto.createDecipher(algorithm, key)
|
var decipher = crypto.createDecipher(algorithm, key)
|
||||||
return JSON.parse(decipher.update(text,'base64','utf8') + decipher.final('utf8'));
|
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 { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate';
|
||||||
export { opener } from './mimetype';
|
export { opener } from './mimetype';
|
||||||
export { debounce, throttle } from './backpressure';
|
export { debounce, throttle } from './backpressure';
|
||||||
export { encrypt, decrypt } from './crypto';
|
export { encrypt, decrypt, bcrypt_password } from './crypto';
|
||||||
export { event } from './events';
|
export { event } from './events';
|
||||||
export { cache } from './cache';
|
export { cache } from './cache';
|
||||||
export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, appendShareToUrl } from './path';
|
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 { leftPad, copyToClipboard } from './common';
|
||||||
export { getMimeType } from './mimetype';
|
export { getMimeType } from './mimetype';
|
||||||
export { settings_get, settings_put } from './settings';
|
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="96x96" href="/assets/logo/favicon-96x96.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/logo/favicon-16x16.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" />
|
<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-TileColor" content="#f2f2f2">
|
||||||
<meta name="msapplication-TileImage" content="/assets/logo/ms-icon-144x144.png">
|
<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">
|
<meta name="description" content="browse your files in the cloud">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,17 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Router from './router';
|
import Router from './router';
|
||||||
|
|
||||||
|
import { Config } from "./model/"
|
||||||
|
|
||||||
import './assets/css/reset.scss';
|
import './assets/css/reset.scss';
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
const className = 'ontouchstart' in window ? 'touch-yes' : 'touch-no';
|
const className = 'ontouchstart' in window ? 'touch-yes' : 'touch-no';
|
||||||
document.body.classList.add(className);
|
document.body.classList.add(className);
|
||||||
|
|
||||||
ReactDOM.render(<Router/>, document.getElementById('main'));
|
Config.refresh().then(() => {
|
||||||
|
ReactDOM.render(<Router/>, document.getElementById('main'));
|
||||||
|
});
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/assets/worker/cache.js').catch(function(error) {
|
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 { Files } from "./files";
|
||||||
export { Session } from './session';
|
export { Session } from "./session";
|
||||||
export { Share } from './share';
|
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(Session.currentUser)
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
let url = '/files/';
|
let url = '/files/';
|
||||||
let path = user.home
|
let path = user.home;
|
||||||
if(path){
|
if(path){
|
||||||
path = path.replace(/^\/?(.*?)\/?$/, "$1");
|
path = path.replace(/^\/?(.*?)\/?$/, "$1");
|
||||||
if(path !== ""){
|
if(path !== ""){
|
||||||
|
|
@ -108,7 +108,7 @@ export class ConnectPage extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="component_page_connect">
|
<div className="component_page_connect">
|
||||||
<NgIf cond={CONFIG["fork_button"]}>
|
<NgIf cond={window.CONFIG["fork_button"]}>
|
||||||
<ForkMe repo="https://github.com/mickael-kerjean/nuage" />
|
<ForkMe repo="https://github.com/mickael-kerjean/nuage" />
|
||||||
</NgIf>
|
</NgIf>
|
||||||
<Container maxWidth="565px">
|
<Container maxWidth="565px">
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,12 @@
|
||||||
|
|
||||||
.component_page_connection_form.form-appear{
|
.component_page_connection_form.form-appear{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: translateX(5px);
|
||||||
}
|
}
|
||||||
.component_page_connection_form.form-appear.form-appear-active{
|
.component_page_connection_form.form-appear.form-appear-active{
|
||||||
opacity: 1;
|
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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { NgIf, Icon } from '../../components/';
|
import { NgIf, Icon, Button } from '../../components/';
|
||||||
import { Share } from '../../model/';
|
import { Share } from '../../model/';
|
||||||
import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/';
|
import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/';
|
||||||
import './share.scss';
|
import './share.scss';
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ export class NewThing extends React.Component {
|
||||||
|
|
||||||
onSearchChange(search){
|
onSearchChange(search){
|
||||||
this.setState({search_keyword: search});
|
this.setState({search_keyword: search});
|
||||||
console.log(search);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,7 @@ export class HomePage extends React.Component {
|
||||||
this.setState({redirection: "/login"});
|
this.setState({redirection: "/login"});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => this.setState({redirection: "/login"}));
|
||||||
this.setState({redirection: "/login"});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
if(this.state.redirection !== null){
|
if(this.state.redirection !== null){
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,6 @@ export class Pager extends React.Component {
|
||||||
this.props.history.push(url);
|
this.props.history.push(url);
|
||||||
if(this.refs.$page) this.refs.$page.blur();
|
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);
|
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)
|
Files.url(this.state.files[preload_index].path)
|
||||||
.then((url) => this.props.emit("media::preload", url))
|
.then((url) => this.props.emit("media::preload", url))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
|
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
|
||||||
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
|
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 { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
|
||||||
import { ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
|
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 {
|
export default class AppRouter extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -16,6 +22,7 @@ export default class AppRouter extends React.Component {
|
||||||
<Route path="/files/:path*" component={FilesPage} />
|
<Route path="/files/:path*" component={FilesPage} />
|
||||||
<Route path="/view/:path*" component={ViewerPage} />
|
<Route path="/view/:path*" component={ViewerPage} />
|
||||||
<Route path="/logout" component={LogoutPage} />
|
<Route path="/logout" component={LogoutPage} />
|
||||||
|
<Route path="/admin" component={AdminPage} />
|
||||||
<Route component={NotFoundPage} />
|
<Route component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.11.4",
|
"babel-cli": "^6.11.4",
|
||||||
"babel-core": "^6.13.2",
|
"babel-core": "^6.13.2",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Config *Config
|
|
||||||
Backend IBackend
|
Backend IBackend
|
||||||
Body map[string]interface{}
|
Body map[string]interface{}
|
||||||
Session map[string]string
|
Session map[string]string
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ func (d *Driver) Get(name string) IBackend {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Drivers() map[string]IBackend {
|
||||||
|
return d.ds
|
||||||
|
}
|
||||||
|
|
||||||
type Nothing struct {}
|
type Nothing struct {}
|
||||||
|
|
||||||
func (b Nothing) Init(params map[string]string, app *App) (IBackend, error) {
|
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 {
|
func (b Nothing) Save(path string, file io.Reader) error {
|
||||||
return NewError("", 401)
|
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)
|
c.Cache = cache.New(retention*time.Minute, cleanup*time.Minute)
|
||||||
return c
|
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
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"os"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var SECRET_KEY string
|
var (
|
||||||
var configPath string = filepath.Join(GetCurrentDir(), CONFIG_PATH + "config.json")
|
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() {
|
func init() {
|
||||||
c := NewConfig()
|
Config = NewConfiguration()
|
||||||
// Let's initialise all our json config stuff
|
Config.Load()
|
||||||
// For some reasons the file will be written bottom up so we start from the end moving up to the top
|
Config.Init()
|
||||||
|
}
|
||||||
|
|
||||||
// Connections
|
func NewConfiguration() Configuration {
|
||||||
if c.Get("connections.0.type").Interface() == nil {
|
return Configuration{
|
||||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "webdav", "label": "Webdav"})
|
mu: sync.Mutex{},
|
||||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "ftp", "label": "FTP"})
|
cache: NewKeyValueStore(),
|
||||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "sftp", "label": "SFTP"})
|
form: []Form{
|
||||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "git", "label": "GIT"})
|
Form{
|
||||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "s3", "label": "S3"})
|
Title: "general",
|
||||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "dropbox", "label": "Dropbox"})
|
Elmnts: []FormElement{
|
||||||
c.Get("connections.-1").Set(map[string]interface{}{"type": "gdrive", "label": "Drive"})
|
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
|
for i := 0; i < len(this.Form); i++ {
|
||||||
c.Get("oauth").Default("")
|
if i == 0 && len(this.Elmnts) == 0 {
|
||||||
|
ret = fmt.Sprintf("%s{", ret)
|
||||||
// Features
|
}
|
||||||
c.Get("features.share.enable").Default(true)
|
ret = ret + this.Form[i].toJSON(fn)
|
||||||
c.Get("features.search.enable").Default(true)
|
if i == len(this.Form) - 1 {
|
||||||
|
ret = fmt.Sprintf("%s}", ret)
|
||||||
|
}
|
||||||
// Log
|
if i != len(this.Form) - 1 {
|
||||||
c.Get("log.telemetry").Default(true)
|
ret = fmt.Sprintf("%s,", ret)
|
||||||
c.Get("log.level").Default("INFO")
|
}
|
||||||
c.Get("log.enable").Default(true)
|
}
|
||||||
|
|
||||||
// Email
|
if len(this.Form) == 0 && len(this.Elmnts) == 0 {
|
||||||
c.Get("email.from").Default("username@gmail.com")
|
ret = fmt.Sprintf("%s{}", ret)
|
||||||
c.Get("email.password").Default("password")
|
}
|
||||||
c.Get("email.username").Default("username@gmail.com")
|
|
||||||
c.Get("email.port").Default(587)
|
return ret
|
||||||
c.Get("email.server").Default("smtp.gmail.com")
|
}
|
||||||
|
|
||||||
// General
|
type FormIterator struct {
|
||||||
c.Get("general.remember_me").Default(true)
|
Path string
|
||||||
c.Get("general.auto_connect").Default(false)
|
*FormElement
|
||||||
c.Get("general.display_hidden").Default(true)
|
}
|
||||||
c.Get("general.fork_button").Default(true)
|
func (this *Form) Iterator() []FormIterator {
|
||||||
c.Get("general.editor").Default("emacs")
|
slice := make([]FormIterator, 0)
|
||||||
if c.Get("general.secret_key").String() == "" {
|
|
||||||
c.Get("general.secret_key").Set(RandomString(16))
|
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 != "" {
|
if env := os.Getenv("APPLICATION_URL"); env != "" {
|
||||||
c.Get("general.host").Set(env)
|
this.Get("general.host").Set(env).String()
|
||||||
} else {
|
|
||||||
c.Get("general.host").Default("http://127.0.0.1:8334")
|
|
||||||
}
|
}
|
||||||
c.Get("general.port").Default(8334)
|
|
||||||
c.Get("general.name").Default("Nuage")
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfig() *Config {
|
if len(this.conn) == 0 {
|
||||||
a := Config{}
|
this.conn = []map[string]interface{}{
|
||||||
return a.load()
|
map[string]interface{}{
|
||||||
}
|
"type": "webdav",
|
||||||
type Config struct {
|
"label": "WebDav",
|
||||||
mu sync.Mutex
|
},
|
||||||
path *string
|
map[string]interface{}{
|
||||||
json string
|
"type": "ftp",
|
||||||
reader gjson.Result
|
"label": "FTP",
|
||||||
}
|
},
|
||||||
|
map[string]interface{}{
|
||||||
func (this *Config) load() *Config {
|
"type": "sftp",
|
||||||
if f, err := os.OpenFile(configPath, os.O_RDONLY, os.ModePerm); err == nil {
|
"label": "SFTP",
|
||||||
j, _ := ioutil.ReadAll(f)
|
},
|
||||||
this.json = string(j)
|
map[string]interface{}{
|
||||||
f.Close()
|
"type": "git",
|
||||||
} else {
|
"label": "GIT",
|
||||||
this.json = `{}`
|
},
|
||||||
|
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 {
|
SECRET_KEY = this.Get("general.secret_key").String()
|
||||||
this.reader = gjson.Parse(this.json)
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *Config) Get(path string) *Config {
|
func (this Configuration) Save() Configuration {
|
||||||
this.path = &path
|
// convert config data to an appropriate json struct
|
||||||
return this
|
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 {
|
// convert back to a map[string]interface{} so that we can stuff in backends config
|
||||||
if this.path == nil {
|
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
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
if val := this.reader.Get(*this.path).Value(); val == nil {
|
// deploy the config in our config.json
|
||||||
this.mu.Lock()
|
file, err := os.Create(configPath)
|
||||||
this.json, _ = sjson.Set(this.json, *this.path, value)
|
if err != nil {
|
||||||
this.reader = gjson.Parse(this.json)
|
|
||||||
this.save()
|
|
||||||
this.mu.Unlock()
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *Config) Set(value interface{}) *Config {
|
|
||||||
if this.path == nil {
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
this.mu.Lock()
|
file.Write(PrettyPrint(j))
|
||||||
this.json, _ = sjson.Set(this.json, *this.path, value)
|
|
||||||
this.reader = gjson.Parse(this.json)
|
|
||||||
this.save()
|
|
||||||
this.mu.Unlock()
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this Config) String() string {
|
func (this Configuration) Export() interface{} {
|
||||||
return this.reader.Get(*this.path).String()
|
return struct {
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
Editor string `json:"editor"`
|
Editor string `json:"editor"`
|
||||||
ForkButton bool `json:"fork_button"`
|
ForkButton bool `json:"fork_button"`
|
||||||
DisplayHidden bool `json:"display_hidden"`
|
DisplayHidden bool `json:"display_hidden"`
|
||||||
|
|
@ -195,14 +334,127 @@ func (this Config) Export() (string, error) {
|
||||||
AutoConnect: this.Get("general.auto_connect").Bool(),
|
AutoConnect: this.Get("general.auto_connect").Bool(),
|
||||||
Name: this.Get("general.name").String(),
|
Name: this.Get("general.name").String(),
|
||||||
RememberMe: this.Get("general.remember_me").Bool(),
|
RememberMe: this.Get("general.remember_me").Bool(),
|
||||||
Connections: this.Get("connections").Interface(),
|
Connections: this.conn,
|
||||||
EnableSearch: this.Get("features.search.enable").Bool(),
|
EnableSearch: this.Get("features.search.enable").Bool(),
|
||||||
EnableShare: this.Get("features.share.enable").Bool(),
|
EnableShare: this.Get("features.share.enable").Bool(),
|
||||||
MimeTypes: AllMimeTypes(),
|
MimeTypes: AllMimeTypes(),
|
||||||
}
|
}
|
||||||
j, err := json.Marshal(publicConf)
|
}
|
||||||
if err != nil {
|
|
||||||
return "", err
|
func (this *Configuration) Get(key string) *Configuration {
|
||||||
}
|
var traverse func (forms *[]Form, path []string) *FormElement
|
||||||
return string(j), nil
|
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) {
|
func TestConfigGet(t *testing.T) {
|
||||||
assert.Equal(t, nil, NewConfig().Get("foo").Interface())
|
c := NewConfiguration()
|
||||||
assert.Equal(t, nil, NewConfig().Get("foo.bar").Interface())
|
assert.Equal(t, nil, c.Get("foo").Interface())
|
||||||
|
assert.Equal(t, nil, c.Get("foo.bar").Interface())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigDefault(t *testing.T) {
|
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").Default("test").String())
|
||||||
assert.Equal(t, "test", c.Get("foo.bar").String())
|
assert.Equal(t, "test", c.Get("foo.bar").String())
|
||||||
assert.Equal(t, "test", c.Get("foo.bar").Default("nope").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) {
|
func TestConfigTypeCase(t *testing.T) {
|
||||||
assert.Equal(t, nil, NewConfig().Get("foo.bar.nil").Default(nil).Interface())
|
c := NewConfiguration()
|
||||||
assert.Equal(t, true, NewConfig().Get("foo.bar").Default(true).Bool())
|
assert.Equal(t, nil, c.Get("foo.bar.nil").Default(nil).Interface())
|
||||||
assert.Equal(t, 10, NewConfig().Get("foo.bar").Default(10).Int())
|
assert.Equal(t, true, c.Get("foo.bar.bool").Default(true).Bool())
|
||||||
assert.Equal(t, "test", NewConfig().Get("foo.bar").Default("test").String())
|
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) {
|
func TestConfigSet(t *testing.T) {
|
||||||
c := NewConfig()
|
assert.Equal(t, "test", Config.Get("foo.bar").Set("test").String())
|
||||||
assert.Equal(t, "test", c.Get("foo.bar").Set("test").String())
|
assert.Equal(t, "valu", Config.Get("foo.bar").Set("valu").String())
|
||||||
assert.Equal(t, "valu", c.Get("foo.bar").Set("valu").String())
|
assert.Equal(t, "valu", Config.Get("foo.bar.test.bar.foo").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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkGetConfigElement(b *testing.B) {
|
func BenchmarkGetConfigElement(b *testing.B) {
|
||||||
c := NewConfig()
|
c := NewConfiguration()
|
||||||
c.Get("foo.bar.test.foo").Set("test")
|
c.Get("foo.bar.test.foo").Set("test")
|
||||||
c.Get("foo.bar.test.bar.foo").Set("valu")
|
c.Get("foo.bar.test.bar.foo").Set("valu")
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
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"
|
APP_VERSION = "v0.3"
|
||||||
CONFIG_PATH = "data/config/"
|
CONFIG_PATH = "data/config/"
|
||||||
PLUGIN_PATH = "data/plugin/"
|
PLUGIN_PATH = "data/plugin/"
|
||||||
|
LOG_PATH = "data/log/"
|
||||||
COOKIE_NAME_AUTH = "auth"
|
COOKIE_NAME_AUTH = "auth"
|
||||||
COOKIE_NAME_PROOF = "proof"
|
COOKIE_NAME_PROOF = "proof"
|
||||||
|
COOKIE_NAME_ADMIN = "admin"
|
||||||
|
COOKIE_PATH_ADMIN = "/admin/api/"
|
||||||
COOKIE_PATH = "/api/"
|
COOKIE_PATH = "/api/"
|
||||||
FILE_INDEX = "./data/public/index.html"
|
FILE_INDEX = "./data/public/index.html"
|
||||||
FILE_ASSETS = "./data/public/"
|
FILE_ASSETS = "./data/public/"
|
||||||
|
URL_SETUP = "/admin/setup"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ func verify(something []byte) ([]byte, error) {
|
||||||
func GenerateID(ctx *App) string {
|
func GenerateID(ctx *App) string {
|
||||||
params := ctx.Session
|
params := ctx.Session
|
||||||
p := "type =>" + params["type"]
|
p := "type =>" + params["type"]
|
||||||
p += "salt => " + ctx.Config.Get("general.secret_key").String()
|
p += "salt => " + SECRET_KEY
|
||||||
p += "host =>" + params["host"]
|
p += "host =>" + params["host"]
|
||||||
p += "hostname =>" + params["hostname"]
|
p += "hostname =>" + params["hostname"]
|
||||||
p += "username =>" + params["username"]
|
p += "username =>" + params["username"]
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ func TestIDGeneration(t *testing.T) {
|
||||||
session["foo"] = "bar"
|
session["foo"] = "bar"
|
||||||
app := &App{
|
app := &App{
|
||||||
Session: session,
|
Session: session,
|
||||||
Config: NewConfig(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
id1 := GenerateID(app)
|
id1 := GenerateID(app)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ var (
|
||||||
ErrPermissionDenied error = NewError("Permission Denied", 403)
|
ErrPermissionDenied error = NewError("Permission Denied", 403)
|
||||||
ErrNotValid error = NewError("Not Valid", 405)
|
ErrNotValid error = NewError("Not Valid", 405)
|
||||||
ErrNotReachable error = NewError("Cannot Reach Destination", 502)
|
ErrNotReachable error = NewError("Cannot Reach Destination", 502)
|
||||||
|
ErrInvalidPassword = NewError("Invalid Password", 403)
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppError struct {
|
type AppError struct {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,25 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
slog "log"
|
slog "log"
|
||||||
|
"fmt"
|
||||||
"time"
|
"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 {
|
type LogEntry struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
|
|
@ -32,28 +48,53 @@ type log struct{
|
||||||
|
|
||||||
func (l *log) Info(format string, v ...interface{}) {
|
func (l *log) Info(format string, v ...interface{}) {
|
||||||
if l.info && l.enable {
|
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{}) {
|
func (l *log) Warning(format string, v ...interface{}) {
|
||||||
if l.warn && l.enable {
|
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{}) {
|
func (l *log) Error(format string, v ...interface{}) {
|
||||||
if l.error && l.enable {
|
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{}) {
|
func (l *log) Debug(format string, v ...interface{}) {
|
||||||
if l.debug && l.enable {
|
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) {
|
func (l *log) SetVisibility(str string) {
|
||||||
switch str {
|
switch str {
|
||||||
case "WARNING":
|
case "WARNING":
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,17 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PluginTypeBackend = "backend"
|
||||||
|
PluginTypeMiddleware = "middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Plugin struct {
|
||||||
|
Type string
|
||||||
|
Enable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type Register struct{}
|
type Register struct{}
|
||||||
type Get 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
|
Save(path string, file io.Reader) error
|
||||||
Touch(path string) error
|
Touch(path string) error
|
||||||
Info() string
|
Info() string
|
||||||
|
LoginForm() Form
|
||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
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 (
|
import (
|
||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
var (
|
||||||
c, err := ctx.Config.Export()
|
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 {
|
if err != nil {
|
||||||
res.Write([]byte("window.CONFIG = {}"))
|
SendErrorResult(res, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res.Write([]byte("window.CONFIG = "))
|
files, err := f.Readdir(0)
|
||||||
res.Write([]byte(c))
|
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))
|
SendErrorResult(res, NewError(err.Error(), 500))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
obfuscate, err := EncryptString(ctx.Config.Get("general.secret_key").String(), string(s))
|
obfuscate, err := EncryptString(SECRET_KEY, string(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, NewError(err.Error(), 500))
|
SendErrorResult(res, NewError(err.Error(), 500))
|
||||||
return
|
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) {
|
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 ctx.Backend != nil {
|
||||||
if obj, ok := ctx.Backend.(interface{ Close() error }); ok {
|
if obj, ok := ctx.Backend.(interface{ Close() error }); ok {
|
||||||
go obj.Close()
|
go obj.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
http.SetCookie(res, &http.Cookie{
|
||||||
http.SetCookie(res, &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)
|
SendSuccessResult(res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if submittedProof.Key != "<nil>" {
|
if submittedProof.Key != "" {
|
||||||
submittedProof.Id = Hash(submittedProof.Key + "::" + submittedProof.Value)
|
submittedProof.Id = Hash(submittedProof.Key + "::" + submittedProof.Value)
|
||||||
verifiedProof = append(verifiedProof, submittedProof)
|
verifiedProof = append(verifiedProof, submittedProof)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,17 @@ func StaticHandler(_path string, ctx App) http.Handler {
|
||||||
|
|
||||||
func DefaultHandler(_path string, ctx App) http.Handler {
|
func DefaultHandler(_path string, ctx App) http.Handler {
|
||||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
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 := res.Header()
|
||||||
header.Set("Content-Type", "text/html")
|
header.Set("Content-Type", "text/html")
|
||||||
|
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
SecureHeader(&header)
|
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
|
p := _path
|
||||||
if _, err := os.Open(path.Join(GetCurrentDir(), p+".gz")); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
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")
|
res.Header().Set("Content-Encoding", "gzip")
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
package ctrl
|
package ctrl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
. "github.com/mickael-kerjean/nuage/server/middleware"
|
. "github.com/mickael-kerjean/nuage/server/middleware"
|
||||||
"github.com/mickael-kerjean/nuage/server/model"
|
"github.com/mickael-kerjean/nuage/server/model"
|
||||||
"github.com/mickael-kerjean/net/webdav"
|
"github.com/mickael-kerjean/net/webdav"
|
||||||
"github.com/mickael-kerjean/mux"
|
"github.com/mickael-kerjean/mux"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,14 +19,28 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
prefix := "/s/" + share_id
|
prefix := "/s/" + share_id
|
||||||
req.Header.Del("Content-Type")
|
req.Header.Del("Content-Type")
|
||||||
|
|
||||||
if req.Method == "GET" {
|
if req.Method == "GET" && req.URL.Path == prefix {
|
||||||
if req.URL.Path == prefix {
|
DefaultHandler(FILE_INDEX, ctx).ServeHTTP(res, req)
|
||||||
DefaultHandler(FILE_INDEX, ctx).ServeHTTP(res, req)
|
return
|
||||||
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
|
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 {
|
if ctx.Session, err = ExtractSession(req, &ctx); err != nil {
|
||||||
http.NotFound(res, req)
|
http.NotFound(res, req)
|
||||||
return
|
return
|
||||||
|
|
@ -38,22 +54,21 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// webdav is WIP
|
Log.Warning("==== REQUEST ('%s'): %s", req.Method, req.URL.Path)
|
||||||
http.NotFound(res, req)
|
//start := time.Now()
|
||||||
return
|
|
||||||
|
|
||||||
h := &webdav.Handler{
|
h := &webdav.Handler{
|
||||||
Prefix: "/s/" + share_id,
|
Prefix: "/s/" + share_id,
|
||||||
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Session["path"]),
|
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Share.Path),
|
||||||
LockSystem: webdav.NewMemLS(),
|
LockSystem: webdav.NewMemLS(),
|
||||||
Logger: func(r *http.Request, err error) {
|
Logger: func(r *http.Request, err error) {
|
||||||
e := func(err error) string{
|
//Log.Info("==== REQUEST ('%s' => %d): %s\n", req.Method, time.Now().Sub(start) / (1000 * 1000), req.URL.Path)
|
||||||
if err != nil {
|
// e := func(err error) string{
|
||||||
return err.Error()
|
// if err != nil {
|
||||||
}
|
// return err.Error()
|
||||||
return "OK"
|
// }
|
||||||
}(err)
|
// return "OK"
|
||||||
Log.Info("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)
|
// }(err)
|
||||||
|
//Log.Info("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
h.ServeHTTP(res, req)
|
h.ServeHTTP(res, req)
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := App{}
|
app := App{}
|
||||||
app.Config = NewConfig()
|
|
||||||
Log.SetVisibility(app.Config.Get("log.level").String())
|
|
||||||
Init(&app)
|
Init(&app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,36 +46,47 @@ func Init(a *App) {
|
||||||
session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET")
|
session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET")
|
||||||
|
|
||||||
files := r.PathPrefix("/api/files").Subrouter()
|
files := r.PathPrefix("/api/files").Subrouter()
|
||||||
files.HandleFunc("/ls", APIHandler(LoggedInOnly(FileLs), *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(FileCat), *a)).Methods("GET")
|
||||||
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
|
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
|
||||||
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
|
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
|
||||||
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
|
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
|
||||||
files.HandleFunc("/mkdir", APIHandler(LoggedInOnly(FileMkdir), *a)).Methods("GET")
|
files.HandleFunc("/mkdir", APIHandler(LoggedInOnly(FileMkdir), *a)).Methods("GET")
|
||||||
files.HandleFunc("/touch", APIHandler(LoggedInOnly(FileTouch), *a)).Methods("GET")
|
files.HandleFunc("/touch", APIHandler(LoggedInOnly(FileTouch), *a)).Methods("GET")
|
||||||
|
|
||||||
share := r.PathPrefix("/api/share").Subrouter()
|
share := r.PathPrefix("/api/share").Subrouter()
|
||||||
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
|
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
|
||||||
share.HandleFunc("/{share}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
share.HandleFunc("/{share}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
||||||
share.HandleFunc("/{share}", APIHandler(ShareDelete, *a)).Methods("DELETE")
|
share.HandleFunc("/{share}", APIHandler(ShareDelete, *a)).Methods("DELETE")
|
||||||
share.HandleFunc("/{share}/proof", APIHandler(ShareVerifyProof, *a)).Methods("POST")
|
share.HandleFunc("/{share}/proof", APIHandler(ShareVerifyProof, *a)).Methods("POST")
|
||||||
|
|
||||||
// WEBDAV
|
// WEBDAV
|
||||||
r.PathPrefix("/s/{share}").Handler(CtxInjector(WebdavHandler, *a))
|
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
|
// 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("/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")
|
r.PathPrefix("/").Handler(DefaultHandler(FILE_INDEX, *a)).Methods("GET")
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: ":" + strconv.Itoa(a.Config.Get("general.port").Int()),
|
Addr: ":" + strconv.Itoa(Config.Get("general.port").Int()),
|
||||||
Handler: r,
|
Handler: r,
|
||||||
}
|
}
|
||||||
Log.Info("STARTING SERVER")
|
|
||||||
|
Log.Stdout("STARTING SERVER")
|
||||||
if err := srv.ListenAndServe(); err != nil {
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
Log.Error("Server start: %v", err)
|
Log.Stdout("Server start: %v", err)
|
||||||
return
|
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
|
var err error
|
||||||
start := time.Now()
|
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 {
|
if ctx.Body, err = ExtractBody(req); err != nil {
|
||||||
SendErrorResult(res, ErrNotValid)
|
SendErrorResult(res, ErrNotValid)
|
||||||
return
|
return
|
||||||
|
|
@ -39,32 +40,16 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
|
||||||
req.Body.Close()
|
req.Body.Close()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if ctx.Config.Get("log.telemetry").Bool() {
|
if Config.Get("log.telemetry").Bool() {
|
||||||
go telemetry(req, &resw, start, ctx.Backend.Info())
|
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)
|
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) {
|
func ExtractBody(req *http.Request) (map[string]interface{}, error) {
|
||||||
var body map[string]interface{}
|
var body map[string]interface{}
|
||||||
|
|
||||||
|
|
@ -90,7 +75,8 @@ func ExtractShare(req *http.Request, ctx *App, share_id string) (Share, error) {
|
||||||
return Share{}, nil
|
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)
|
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 != "" {
|
if env := os.Getenv("DROPBOX_CLIENT_ID"); env != "" {
|
||||||
backend.ClientId = env
|
backend.ClientId = env
|
||||||
} else {
|
} 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"]
|
backend.Bearer = params["bearer"]
|
||||||
|
|
||||||
if backend.ClientId == "" {
|
if backend.ClientId == "" {
|
||||||
|
|
@ -45,6 +45,23 @@ func (d Dropbox) Info() string {
|
||||||
return "dropbox"
|
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 {
|
func (d Dropbox) OAuthURL() string {
|
||||||
url := "https://www.dropbox.com/oauth2/authorize?"
|
url := "https://www.dropbox.com/oauth2/authorize?"
|
||||||
url += "client_id=" + d.ClientId
|
url += "client_id=" + d.ClientId
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,57 @@ func (f Ftp) Info() string {
|
||||||
return "ftp"
|
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) {
|
func (f Ftp) Home() (string, error) {
|
||||||
return f.client.Getwd()
|
return f.client.Getwd()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ func (g GDrive) Init(params map[string]string, app *App) (IBackend, error) {
|
||||||
|
|
||||||
config := &oauth2.Config{
|
config := &oauth2.Config{
|
||||||
Endpoint: google.Endpoint,
|
Endpoint: google.Endpoint,
|
||||||
ClientID: app.Config.Get("oauth.gdrive.client_id").Default(os.Getenv("GDRIVE_CLIENT_ID")).String(),
|
ClientID: Config.Get("auth.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(),
|
ClientSecret: Config.Get("auth.gdrive.client_secret").Default(os.Getenv("GDRIVE_CLIENT_SECRET")).String(),
|
||||||
RedirectURL: app.Config.Get("general.host").String() + "/login",
|
RedirectURL: Config.Get("general.host").String() + "/login",
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/drive"},
|
Scopes: []string{"https://www.googleapis.com/auth/drive"},
|
||||||
}
|
}
|
||||||
if config.ClientID == "" {
|
if config.ClientID == "" {
|
||||||
|
|
@ -71,6 +71,23 @@ func (g GDrive) Info() string {
|
||||||
return "googledrive"
|
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 {
|
func (g GDrive) OAuthURL() string {
|
||||||
return g.Config.AuthCodeURL("googledrive", oauth2.AccessTypeOnline)
|
return g.Config.AuthCodeURL("googledrive", oauth2.AccessTypeOnline)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,93 @@ func (g Git) Info() string {
|
||||||
return "git"
|
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) {
|
func (g Git) Ls(path string) ([]os.FileInfo, error) {
|
||||||
g.git.refresh()
|
g.git.refresh()
|
||||||
p, err := g.path(path)
|
p, err := g.path(path)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,58 @@ func (s S3Backend) Info() string {
|
||||||
return "s3"
|
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 {
|
func (s S3Backend) Meta(path string) Metadata {
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
return Metadata{
|
return Metadata{
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,58 @@ func (b Sftp) Info() string {
|
||||||
return "sftp"
|
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) {
|
func (b Sftp) Home() (string, error) {
|
||||||
cwd, err := b.SFTPClient.Getwd()
|
cwd, err := b.SFTPClient.Getwd()
|
||||||
if err != nil {
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, b.err(err)
|
return nil, b.err(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,45 @@ func (w WebDav) Info() string {
|
||||||
return "webdav"
|
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) {
|
func (w WebDav) Ls(path string) ([]os.FileInfo, error) {
|
||||||
files := make([]os.FileInfo, 0)
|
files := make([]os.FileInfo, 0)
|
||||||
query := `<d:propfind xmlns:d='DAV:'>
|
query := `<d:propfind xmlns:d='DAV:'>
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,28 @@ import (
|
||||||
|
|
||||||
func NewBackend(ctx *App, conn map[string]string) (IBackend, error) {
|
func NewBackend(ctx *App, conn map[string]string) (IBackend, error) {
|
||||||
isAllowed := func() bool {
|
isAllowed := func() bool {
|
||||||
ret := false
|
return true
|
||||||
var conns [] struct {
|
// ret := false
|
||||||
Type string `json:"type"`
|
// var conns [] struct {
|
||||||
Hostname string `json:"hostname"`
|
// Type string `json:"type"`
|
||||||
Path string `json:"path"`
|
// Hostname string `json:"hostname"`
|
||||||
}
|
// Path string `json:"path"`
|
||||||
ctx.Config.Get("connections").Scan(&conns)
|
// }
|
||||||
for i := range conns {
|
// Config.Get("connections").Interface()
|
||||||
if conns[i].Type == conn["type"] {
|
// Config.Get("connections").Scan(&conns)
|
||||||
if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] {
|
// for i := range conns {
|
||||||
continue
|
// if conns[i].Type == conn["type"] {
|
||||||
} else if conns[i].Path != "" && conns[i].Path != conn["path"] {
|
// if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] {
|
||||||
continue
|
// continue
|
||||||
} else {
|
// } else if conns[i].Path != "" && conns[i].Path != conn["path"] {
|
||||||
ret = true
|
// continue
|
||||||
break
|
// } else {
|
||||||
}
|
// ret = true
|
||||||
}
|
// break
|
||||||
}
|
// }
|
||||||
return ret
|
// }
|
||||||
|
// }
|
||||||
|
// return ret
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if isAllowed == false {
|
if isAllowed == false {
|
||||||
|
|
@ -57,6 +59,9 @@ func MapStringInterfaceToMapStringString(m map[string]interface{}) map[string]st
|
||||||
res := make(map[string]string)
|
res := make(map[string]string)
|
||||||
for key, value := range m {
|
for key, value := range m {
|
||||||
res[key] = fmt.Sprintf("%v", value)
|
res[key] = fmt.Sprintf("%v", value)
|
||||||
|
if res[key] == "<nil>" {
|
||||||
|
res[key] = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
|
||||||
}
|
}
|
||||||
time.Sleep(1000 * time.Millisecond)
|
time.Sleep(1000 * time.Millisecond)
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(*s.Password), []byte(proof.Value)); err != nil {
|
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
|
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")
|
p.Message = NewString("We've sent you a message with a verification code")
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
var email struct {
|
email := struct {
|
||||||
Hostname string `json:"server"`
|
Hostname string `json:"server"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
}
|
}{
|
||||||
if err := ctx.Config.Get("email").Scan(&email); err != nil {
|
Hostname: Config.Get("email.server").String(),
|
||||||
Log.Error("ERROR(%+v)", err)
|
Port: Config.Get("email.port").Int(),
|
||||||
return p, nil
|
Username: Config.Get("email.username").String(),
|
||||||
|
Password: Config.Get("email.password").String(),
|
||||||
|
From: Config.Get("email.from").String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const DAVCachePath = "data/cache/webdav/"
|
const DAVCachePath = "data/cache/webdav/"
|
||||||
|
|
@ -23,7 +22,6 @@ func init() {
|
||||||
type WebdavFs struct {
|
type WebdavFs struct {
|
||||||
backend IBackend
|
backend IBackend
|
||||||
path string
|
path string
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebdavFs(b IBackend, path string) WebdavFs {
|
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 {
|
func (fs WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||||
|
Log.Info("MKDIR ('%s')", name)
|
||||||
if name = fs.resolve(name); name == "" {
|
if name = fs.resolve(name); name == "" {
|
||||||
return os.ErrInvalid
|
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) {
|
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
|
return NewWebdavNode(name, fs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs WebdavFs) RemoveAll(ctx context.Context, name string) error {
|
func (fs WebdavFs) RemoveAll(ctx context.Context, name string) error {
|
||||||
|
Log.Info("RM ('%s')", name)
|
||||||
if name = fs.resolve(name); name == "" {
|
if name = fs.resolve(name); name == "" {
|
||||||
return os.ErrInvalid
|
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 {
|
func (fs WebdavFs) Rename(ctx context.Context, oldName, newName string) error {
|
||||||
|
Log.Info("MV ('%s' => '%s')", oldName, newName)
|
||||||
if oldName = fs.resolve(oldName); oldName == "" {
|
if oldName = fs.resolve(oldName); oldName == "" {
|
||||||
return os.ErrInvalid
|
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) {
|
func (fs WebdavFs) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||||
|
Log.Info("STAT ('%s')", name)
|
||||||
if name = fs.resolve(name); name == "" {
|
if name = fs.resolve(name); name == "" {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +102,7 @@ func NewWebdavNode(name string, fs WebdavFs) *WebdavNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
|
func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
Log.Info(" => READ_DIR ('%s')", w.path)
|
||||||
var path string
|
var path string
|
||||||
if path = w.fs.resolve(w.path); path == "" {
|
if path = w.fs.resolve(w.path); path == "" {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
|
|
@ -107,27 +111,29 @@ func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebdavNode) Stat() (os.FileInfo, error) {
|
func (w *WebdavNode) Stat() (os.FileInfo, error) {
|
||||||
if w.filewrite != nil {
|
Log.Info(" => STAT ('%s')", w.path)
|
||||||
var path string
|
// if w.filewrite != nil {
|
||||||
var err error
|
// var path stringc
|
||||||
|
// var err error
|
||||||
|
|
||||||
if path = w.fs.resolve(w.path); path == "" {
|
// if path = w.fs.resolve(w.path); path == "" {
|
||||||
return nil, os.ErrInvalid
|
// return nil, os.ErrInvalid
|
||||||
}
|
// }
|
||||||
name := w.filewrite.Name()
|
// name := w.filewrite.Name()
|
||||||
w.filewrite.Close()
|
// w.filewrite.Close()
|
||||||
if w.filewrite, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm); err != nil {
|
// if w.filewrite, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm); err != nil {
|
||||||
return nil, os.ErrInvalid
|
// return nil, os.ErrInvalid
|
||||||
}
|
// }
|
||||||
|
|
||||||
if err = w.fs.backend.Save(path, w.filewrite); err != nil {
|
// if err = w.fs.backend.Save(path, w.filewrite); err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return w.fs.Stat(context.Background(), w.path)
|
return w.fs.Stat(context.Background(), w.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebdavNode) Close() error {
|
func (w *WebdavNode) Close() error {
|
||||||
|
Log.Info(" => CLOSE ('%s')", w.path)
|
||||||
if w.fileread != nil {
|
if w.fileread != nil {
|
||||||
if err := w.cleanup(w.fileread); err != nil {
|
if err := w.cleanup(w.fileread); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -135,15 +141,27 @@ func (w *WebdavNode) Close() error {
|
||||||
w.fileread = nil
|
w.fileread = nil
|
||||||
}
|
}
|
||||||
if w.filewrite != 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
|
return err
|
||||||
}
|
}
|
||||||
w.filewrite = nil
|
reader.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebdavNode) Read(p []byte) (int, error) {
|
func (w *WebdavNode) Read(p []byte) (int, error) {
|
||||||
|
Log.Info(" => READ ('%s')", w.path)
|
||||||
if w.fileread != nil {
|
if w.fileread != nil {
|
||||||
return w.fileread.Read(p)
|
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) {
|
func (w *WebdavNode) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
Log.Info(" => SEEK ('%s')", w.path)
|
||||||
var path string
|
var path string
|
||||||
var err error
|
var err error
|
||||||
if path = w.fs.resolve(w.path); path == "" {
|
if path = w.fs.resolve(w.path); path == "" {
|
||||||
return -1, os.ErrInvalid
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if w.fileread == nil {
|
if w.fileread == nil {
|
||||||
var reader io.Reader
|
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
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
if reader, err = w.fs.backend.Cat(path); err != nil {
|
if reader, err = w.fs.backend.Cat(path); err != nil {
|
||||||
return 0, os.ErrInvalid
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
io.Copy(w.fileread, reader)
|
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)
|
return w.fileread.Seek(offset, whence)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebdavNode) Write(p []byte) (int, error) {
|
func (w *WebdavNode) Write(p []byte) (int, error) {
|
||||||
|
Log.Info(" => WRITE ('%s')", w.path)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if w.filewrite == nil {
|
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
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,11 +207,7 @@ func (w *WebdavNode) Write(p []byte) (int, error) {
|
||||||
|
|
||||||
func (w *WebdavNode) cleanup(file *os.File) error {
|
func (w *WebdavNode) cleanup(file *os.File) error {
|
||||||
name := file.Name()
|
name := file.Name()
|
||||||
if err := file.Close(); err != nil {
|
file.Close();
|
||||||
return err
|
os.Remove(name);
|
||||||
}
|
|
||||||
if err := os.Remove(name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ func init() {
|
||||||
}
|
}
|
||||||
files, err := file.Readdir(0)
|
files, err := file.Readdir(0)
|
||||||
|
|
||||||
c := NewConfig()
|
|
||||||
for i:=0; i < len(files); i++ {
|
for i:=0; i < len(files); i++ {
|
||||||
name := files[i].Name()
|
name := files[i].Name()
|
||||||
if strings.HasPrefix(name, ".") {
|
if strings.HasPrefix(name, ".") {
|
||||||
|
|
@ -34,8 +33,8 @@ func init() {
|
||||||
Log.Warning("Can't register plugin: %s => %v", name, err)
|
Log.Warning("Can't register plugin: %s => %v", name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if obj, ok := fn.(func(config *Config)); ok {
|
if obj, ok := fn.(func(config *Configuration)); ok {
|
||||||
obj(c)
|
obj(&Config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue