mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-01 19:32:27 +08:00
feature (Share): workable version for sharing
This commit is contained in:
@ -50,11 +50,15 @@
|
|||||||
--secondary: #466372;
|
--secondary: #466372;
|
||||||
--emphasis-secondary: #466372;
|
--emphasis-secondary: #466372;
|
||||||
--light: #909090;
|
--light: #909090;
|
||||||
|
<<<<<<< HEAD
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
--super-light: #f4f4f4;
|
--super-light: #f4f4f4;
|
||||||
=======
|
=======
|
||||||
--super-light: #F7F9FA;
|
--super-light: #F7F9FA;
|
||||||
>>>>>>> 801aef8... improvement (incremental): update colors and improve page when current folder is empty
|
>>>>>>> 801aef8... improvement (incremental): update colors and improve page when current folder is empty
|
||||||
|
=======
|
||||||
|
--super-light: #f9fafc;
|
||||||
|
>>>>>>> 384b3e0... feature (Share): workable version for sharing
|
||||||
--error: #f26d6d;
|
--error: #f26d6d;
|
||||||
--success: #63d9b1;
|
--success: #63d9b1;
|
||||||
--dark: #313538;
|
--dark: #313538;
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export class BreadCrumb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(Element) {
|
render(Element) {
|
||||||
if(location.search === "?nav=false") return null;
|
if(new window.URL(location.href).searchParams.get("nav") === "false") return null;
|
||||||
|
|
||||||
const Path = Element? Element : PathElement;
|
const Path = Element? Element : PathElement;
|
||||||
return (
|
return (
|
||||||
@ -136,7 +136,7 @@ export class PathElementWrapper extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<li className={className}>
|
<li className={className}>
|
||||||
<NgIf cond={this.props.isLast === false}>
|
<NgIf cond={this.props.isLast === false}>
|
||||||
<Link to={"/files" + this.props.path.full || "/"} className="label">
|
<Link to={"/files" + (this.props.path.full || "/") + location.search} className="label">
|
||||||
<NgIf cond={this.props.path.minify !== true}>
|
<NgIf cond={this.props.path.minify !== true}>
|
||||||
{this.limitSize(this.props.path.label)}
|
{this.limitSize(this.props.path.label)}
|
||||||
</NgIf>
|
</NgIf>
|
||||||
|
|||||||
@ -23,4 +23,6 @@ button{
|
|||||||
background: var(--emphasis);
|
background: var(--emphasis);
|
||||||
color: white
|
color: white
|
||||||
}
|
}
|
||||||
|
&.transparent{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,10 @@ export class Container extends React.Component {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {};
|
const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {};
|
||||||
|
let className = "component_container";
|
||||||
|
if(this.props.className) className += " "+this.props.className;
|
||||||
return (
|
return (
|
||||||
<div className="component_container" style={style}>
|
<div className={className} style={style}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Session } from '../model/';
|
import { Session } from '../model/';
|
||||||
import { Container, Loader, Icon } from '../components/';
|
import { Container, Loader, Icon } from '../components/';
|
||||||
import { memory } from '../helpers/';
|
import { memory, currentShare } from '../helpers/';
|
||||||
|
|
||||||
import '../pages/error.scss';
|
import '../pages/error.scss';
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ export function LoggedInOnly(WrappedComponent){
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
if(this.state.is_logged_in === false){
|
if(this.state.is_logged_in === false && currentShare() === null){
|
||||||
Session.currentUser().then((res) => {
|
Session.currentUser().then((res) => {
|
||||||
if(res.is_authenticated === false){
|
if(res.is_authenticated === false){
|
||||||
this.props.error({message: "Authentication Required"});
|
this.props.error({message: "Authentication Required"});
|
||||||
@ -38,7 +38,7 @@ export function LoggedInOnly(WrappedComponent){
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
if(this.state.is_logged_in === true){
|
if(this.state.is_logged_in === true || currentShare() !== null){
|
||||||
return <WrappedComponent {...this.props} />;
|
return <WrappedComponent {...this.props} />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -65,7 +65,7 @@ export function ErrorPage(WrappedComponent){
|
|||||||
const message = this.state.error.message || "There is nothing in here";
|
const message = this.state.error.message || "There is nothing in here";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link to="/" className="backnav">
|
<Link to={`/${window.location.search}`} className="backnav">
|
||||||
<Icon name="arrow_left" />home
|
<Icon name="arrow_left" />home
|
||||||
</Link>
|
</Link>
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ function Data(){
|
|||||||
this._init();
|
this._init();
|
||||||
}
|
}
|
||||||
|
|
||||||
const DB_VERSION = 2;
|
const DB_VERSION = 3;
|
||||||
|
|
||||||
Data.prototype._init = function(){
|
Data.prototype._init = function(){
|
||||||
const request = indexedDB.open('nuage', DB_VERSION);
|
const request = indexedDB.open('nuage', DB_VERSION);
|
||||||
@ -27,30 +27,34 @@ Data.prototype._setup = function(e){
|
|||||||
let store;
|
let store;
|
||||||
let db = e.target.result;
|
let db = e.target.result;
|
||||||
|
|
||||||
if(e.oldVersion == 1){
|
if(e.oldVersion == 1) {
|
||||||
// we've change the schema on v2 adding an index, let's flush
|
// we've change the schema on v2 adding an index, let's flush
|
||||||
// to make sure everything will be fine
|
// to make sure everything will be fine
|
||||||
db.deleteObjectStore(this.FILE_PATH);
|
db.deleteObjectStore(this.FILE_PATH);
|
||||||
db.deleteObjectStore(this.FILE_CONTENT);
|
db.deleteObjectStore(this.FILE_CONTENT);
|
||||||
|
}else if(e.oldVersion == 2){
|
||||||
|
// we've change the primary key to be a (path,share)
|
||||||
|
db.deleteObjectStore(this.FILE_PATH);
|
||||||
|
db.deleteObjectStore(this.FILE_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
store = db.createObjectStore(this.FILE_PATH, {keyPath: "path"});
|
store = db.createObjectStore(this.FILE_PATH, {keyPath: ["share", "path"]});
|
||||||
store.createIndex("idx_path", "path", { unique: true });
|
store.createIndex("idx_path", ["share", "path"], { unique: true });
|
||||||
|
|
||||||
store = db.createObjectStore(this.FILE_CONTENT, {keyPath: "path"});
|
store = db.createObjectStore(this.FILE_CONTENT, {keyPath: ["share", "path"]});
|
||||||
store.createIndex("idx_path", "path", { unique: true });
|
store.createIndex("idx_path", ["share", "path"], { unique: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fetch a record using its path, can be whether a file path or content
|
* Fetch a record using its path, can be whether a file path or content
|
||||||
*/
|
*/
|
||||||
Data.prototype.get = function(type, path){
|
Data.prototype.get = function(type, key){
|
||||||
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
|
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
|
||||||
|
|
||||||
return this.db.then((db) => {
|
return this.db.then((db) => {
|
||||||
const tx = db.transaction(type, "readonly");
|
const tx = db.transaction(type, "readonly");
|
||||||
const store = tx.objectStore(type);
|
const store = tx.objectStore(type);
|
||||||
const query = store.get(path);
|
const query = store.get(key);
|
||||||
return new Promise((done, error) => {
|
return new Promise((done, error) => {
|
||||||
query.onsuccess = (e) => {
|
query.onsuccess = (e) => {
|
||||||
let data = query.result;
|
let data = query.result;
|
||||||
@ -61,13 +65,13 @@ Data.prototype.get = function(type, path){
|
|||||||
}).catch(() => Promise.resolve(null));
|
}).catch(() => Promise.resolve(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.update = function(type, path, fn, exact = true){
|
Data.prototype.update = function(type, key, fn, exact = true){
|
||||||
return this.db.then((db) => {
|
return this.db.then((db) => {
|
||||||
const tx = db.transaction(type, "readwrite");
|
const tx = db.transaction(type, "readwrite");
|
||||||
const store = tx.objectStore(type);
|
const store = tx.objectStore(type);
|
||||||
const range = exact === true? IDBKeyRange.only(path) : IDBKeyRange.bound(
|
const range = exact === true? IDBKeyRange.only(key) : IDBKeyRange.bound(
|
||||||
path,
|
[key[0], key[1]],
|
||||||
path+'\uFFFF',
|
[key[0], key[1]+'\uFFFF'],
|
||||||
false, true
|
false, true
|
||||||
);
|
);
|
||||||
const request = store.openCursor(range);
|
const request = store.openCursor(range);
|
||||||
@ -77,7 +81,7 @@ Data.prototype.update = function(type, path, fn, exact = true){
|
|||||||
const cursor = event.target.result;
|
const cursor = event.target.result;
|
||||||
if(!cursor) return done(new_data);
|
if(!cursor) return done(new_data);
|
||||||
new_data = fn(cursor.value || null);
|
new_data = fn(cursor.value || null);
|
||||||
cursor.delete(cursor.value.path);
|
cursor.delete([key[0], cursor.value.path]);
|
||||||
store.put(new_data);
|
store.put(new_data);
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
};
|
};
|
||||||
@ -86,11 +90,11 @@ Data.prototype.update = function(type, path, fn, exact = true){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Data.prototype.upsert = function(type, path, fn){
|
Data.prototype.upsert = function(type, key, fn){
|
||||||
return this.db.then((db) => {
|
return this.db.then((db) => {
|
||||||
const tx = db.transaction(type, "readwrite");
|
const tx = db.transaction(type, "readwrite");
|
||||||
const store = tx.objectStore(type);
|
const store = tx.objectStore(type);
|
||||||
const query = store.get(path);
|
const query = store.get(key);
|
||||||
return new Promise((done, error) => {
|
return new Promise((done, error) => {
|
||||||
query.onsuccess = (e) => {
|
query.onsuccess = (e) => {
|
||||||
const new_data = fn(query.result || null);
|
const new_data = fn(query.result || null);
|
||||||
@ -105,7 +109,7 @@ Data.prototype.upsert = function(type, path, fn){
|
|||||||
}).catch(() => Promise.resolve(null));
|
}).catch(() => Promise.resolve(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.add = function(type, path, data){
|
Data.prototype.add = function(type, key, data){
|
||||||
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
|
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
|
||||||
|
|
||||||
return this.db.then((db) => {
|
return this.db.then((db) => {
|
||||||
@ -119,28 +123,28 @@ Data.prototype.add = function(type, path, data){
|
|||||||
}).catch(() => Promise.resolve(null));
|
}).catch(() => Promise.resolve(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.remove = function(type, path, exact = true){
|
Data.prototype.remove = function(type, key, exact = true){
|
||||||
return this.db.then((db) => {
|
return this.db.then((db) => {
|
||||||
const tx = db.transaction(type, "readwrite");
|
const tx = db.transaction(type, "readwrite");
|
||||||
const store = tx.objectStore(type);
|
const store = tx.objectStore(type);
|
||||||
|
|
||||||
if(exact === true){
|
if(exact === true){
|
||||||
const req = store.delete(path);
|
const req = store.delete(key);
|
||||||
return new Promise((done, err) => {
|
return new Promise((done, err) => {
|
||||||
req.onsuccess = () => done();
|
req.onsuccess = () => done();
|
||||||
req.onerror = err;
|
req.onerror = err;
|
||||||
});
|
});
|
||||||
}else{
|
}else{
|
||||||
const request = store.openCursor(IDBKeyRange.bound(
|
const request = store.openCursor(IDBKeyRange.bound(
|
||||||
path,
|
[key[0], key[1]],
|
||||||
path+'\uFFFF',
|
[key[0], key[1]+'\uFFFF'],
|
||||||
true, true
|
true, true
|
||||||
));
|
));
|
||||||
return new Promise((done, err) => {
|
return new Promise((done, err) => {
|
||||||
request.onsuccess = function(event) {
|
request.onsuccess = function(event) {
|
||||||
const cursor = event.target.result;
|
const cursor = event.target.result;
|
||||||
if(cursor){
|
if(cursor){
|
||||||
cursor.delete(cursor.value.path);
|
cursor.delete([key[0], cursor.value.path]);
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
}else{
|
}else{
|
||||||
done();
|
done();
|
||||||
@ -151,12 +155,15 @@ Data.prototype.remove = function(type, path, exact = true){
|
|||||||
}).catch(() => Promise.resolve(null));
|
}).catch(() => Promise.resolve(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.fetchAll = function(fn, type = this.FILE_PATH, key = "/"){
|
Data.prototype.fetchAll = function(fn, type = this.FILE_PATH, key){
|
||||||
return this.db.then((db) => {
|
return this.db.then((db) => {
|
||||||
const tx = db.transaction([type], "readonly");
|
const tx = db.transaction([type], "readonly");
|
||||||
const store = tx.objectStore(type);
|
const store = tx.objectStore(type);
|
||||||
const index = store.index("idx_path");
|
const index = store.index("idx_path");
|
||||||
const request = index.openCursor(IDBKeyRange.bound(key, key+("z".repeat(5000))));
|
const request = index.openCursor(IDBKeyRange.bound(
|
||||||
|
[key[0], key[1]],
|
||||||
|
[key[0], key[1]+("z".repeat(5000))]
|
||||||
|
));
|
||||||
|
|
||||||
return new Promise((done, error) => {
|
return new Promise((done, error) => {
|
||||||
request.onsuccess = function(event) {
|
request.onsuccess = function(event) {
|
||||||
|
|||||||
@ -2,3 +2,16 @@ export function leftPad(str, length, pad = "0"){
|
|||||||
if(typeof str !== 'string' || typeof pad !== 'string' || str.length >= length || !pad.length > 0) return str;
|
if(typeof str !== 'string' || typeof pad !== 'string' || str.length >= length || !pad.length > 0) return str;
|
||||||
return leftPad(pad + str, length, pad);
|
return leftPad(pad + str, length, pad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copyToClipboard (str){
|
||||||
|
if(!str) return
|
||||||
|
let $input = document.createElement("input");
|
||||||
|
$input.setAttribute("type", "text");
|
||||||
|
$input.setAttribute("style", "position: absolute; top:0;left:0;background:red")
|
||||||
|
$input.setAttribute("display", "none");
|
||||||
|
document.body.appendChild($input);
|
||||||
|
$input.value = str;
|
||||||
|
$input.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
$input.remove();
|
||||||
|
}
|
||||||
|
|||||||
@ -4,13 +4,13 @@ export { debounce, throttle } from './backpressure';
|
|||||||
export { encrypt, decrypt } from './crypto';
|
export { encrypt, decrypt } from './crypto';
|
||||||
export { event } from './events';
|
export { event } from './events';
|
||||||
export { cache } from './cache';
|
export { cache } from './cache';
|
||||||
export { pathBuilder, basename, dirname, absoluteToRelative, filetype } from './path';
|
export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, appendShareToUrl } from './path';
|
||||||
export { memory } from './memory';
|
export { memory } from './memory';
|
||||||
export { prepare } from './navigate';
|
export { prepare } from './navigate';
|
||||||
export { invalidate, http_get, http_post, http_delete } from './ajax';
|
export { invalidate, http_get, http_post, http_delete } from './ajax';
|
||||||
export { prompt, alert, confirm } from './popup';
|
export { prompt, alert, confirm } from './popup';
|
||||||
export { notify } from './notify';
|
export { notify } from './notify';
|
||||||
export { gid, randomString } from './random';
|
export { gid, randomString } from './random';
|
||||||
export { leftPad } 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';
|
||||||
|
|||||||
@ -35,3 +35,19 @@ export function absoluteToRelative(from, to){
|
|||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function currentShare(){
|
||||||
|
return new window.URL(location.href).searchParams.get("share") || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendShareToUrl(link) {
|
||||||
|
let url = new window.URL(location.href);
|
||||||
|
let share = url.searchParams.get("share");
|
||||||
|
|
||||||
|
if(share){
|
||||||
|
url = new window.URL(location.origin + link)
|
||||||
|
url.searchParams.set("share", share)
|
||||||
|
return url.pathname + url.search
|
||||||
|
}
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { http_get, http_post, prepare, basename, dirname, pathBuilder } from '../helpers/';
|
import { http_get, http_post, prepare, basename, dirname, pathBuilder } from '../helpers/';
|
||||||
import { filetype } from '../helpers/';
|
import { filetype, currentShare, appendShareToUrl } from '../helpers/';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { cache } from '../helpers/';
|
import { cache } from '../helpers/';
|
||||||
@ -46,16 +46,17 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ls_from_http(path){
|
_ls_from_http(path){
|
||||||
const url = '/api/files/ls?path='+prepare(path);
|
let url = appendShareToUrl('/api/files/ls?path='+prepare(path));
|
||||||
|
|
||||||
return http_get(url).then((response) => {
|
return http_get(url).then((response) => {
|
||||||
return cache.upsert(cache.FILE_PATH, path, (_files) => {
|
return cache.upsert(cache.FILE_PATH, [currentShare(), path], (_files) => {
|
||||||
let store = Object.assign({
|
let store = Object.assign({
|
||||||
|
share: currentShare(),
|
||||||
path: path,
|
path: path,
|
||||||
results: null,
|
results: null,
|
||||||
access_count: 0,
|
access_count: 0,
|
||||||
metadata: null
|
metadata: null
|
||||||
}, _files);
|
}, _files);
|
||||||
store.access_count += 1;
|
|
||||||
store.results = response.results || [];
|
store.results = response.results || [];
|
||||||
store.results = store.results.map((f) => {
|
store.results = store.results.map((f) => {
|
||||||
f.path = pathBuilder(path, f.name, f.type);
|
f.path = pathBuilder(path, f.name, f.type);
|
||||||
@ -64,6 +65,8 @@ class FileSystem{
|
|||||||
store.metadata = response.metadata;
|
store.metadata = response.metadata;
|
||||||
|
|
||||||
if(_files && _files.results){
|
if(_files && _files.results){
|
||||||
|
store.access_count = _files.access_count;
|
||||||
|
|
||||||
// find out which entry we want to keep from the cache
|
// find out which entry we want to keep from the cache
|
||||||
let _files_virtual_to_keep = _files.results.filter((file) => {
|
let _files_virtual_to_keep = _files.results.filter((file) => {
|
||||||
return file.icon === 'loading';
|
return file.icon === 'loading';
|
||||||
@ -101,44 +104,47 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ls_from_cache(path, _record_access = false){
|
_ls_from_cache(path, _record_access = false){
|
||||||
if(_record_access === false){
|
return cache.get(cache.FILE_PATH, [currentShare(), path]).then((response) => {
|
||||||
return cache.get(cache.FILE_PATH, path).then((response) => {
|
if(!response || !response.results) return null;
|
||||||
if(!response || !response.results) return null;
|
if(this.current_path === path){
|
||||||
if(this.current_path === path){
|
this.obs && this.obs.next({
|
||||||
this.obs && this.obs.next({
|
status: 'ok',
|
||||||
status: 'ok',
|
results: response.results,
|
||||||
results: response.results,
|
metadata: response.metadata
|
||||||
metadata: response.metadata
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}).then((e) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if(_record_access === true){
|
||||||
|
cache.upsert(cache.FILE_PATH, [currentShare(), path], (response) => {
|
||||||
|
if(!response || !response.results) return null;
|
||||||
|
if(this.current_path === path){
|
||||||
|
this.obs && this.obs.next({
|
||||||
|
status: 'ok',
|
||||||
|
results: response.results,
|
||||||
|
metadata: response.metadata
|
||||||
|
});
|
||||||
|
}
|
||||||
|
response.last_access = new Date();
|
||||||
|
response.access_count += 1;
|
||||||
|
return response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return response;
|
|
||||||
});
|
});
|
||||||
}else{
|
return Promise.resolve(e);
|
||||||
return cache.upsert(cache.FILE_PATH, path, (response) => {
|
});
|
||||||
if(!response || !response.results) return null;
|
|
||||||
if(this.current_path === path){
|
|
||||||
this.obs && this.obs.next({
|
|
||||||
status: 'ok',
|
|
||||||
results: response.results,
|
|
||||||
metadata: response.metadata
|
|
||||||
});
|
|
||||||
}
|
|
||||||
response.last_access = new Date();
|
|
||||||
response.access_count += 1;
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rm(path){
|
rm(path){
|
||||||
const url = '/api/files/rm?path='+prepare(path);
|
const url = appendShareToUrl('/api/files/rm?path='+prepare(path));
|
||||||
return this._replace(path, 'loading')
|
return this._replace(path, 'loading')
|
||||||
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
|
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
|
||||||
.then(() => http_get(url))
|
.then(() => http_get(url))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return cache.remove(cache.FILE_CONTENT, path)
|
return cache.remove(cache.FILE_CONTENT, [currentShare(), path])
|
||||||
.then(cache.remove(cache.FILE_CONTENT, path, false))
|
.then(cache.remove(cache.FILE_CONTENT, [currentShare(), path], false))
|
||||||
.then(cache.remove(cache.FILE_PATH, dirname(path), false))
|
.then(cache.remove(cache.FILE_PATH, [currentShare(), dirname(path)], false))
|
||||||
.then(this._remove(path, 'loading'))
|
.then(this._remove(path, 'loading'))
|
||||||
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
|
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
|
||||||
})
|
})
|
||||||
@ -150,14 +156,15 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
cat(path){
|
cat(path){
|
||||||
const url = '/api/files/cat?path='+prepare(path);
|
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||||
return http_get(url, 'raw')
|
return http_get(url, 'raw')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if(this.is_binary(res) === true){
|
if(this.is_binary(res) === true){
|
||||||
return Promise.reject({code: 'BINARY_FILE'});
|
return Promise.reject({code: 'BINARY_FILE'});
|
||||||
}
|
}
|
||||||
return cache.upsert(cache.FILE_CONTENT, path, (response) => {
|
return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => {
|
||||||
let file = response? response : {
|
let file = response? response : {
|
||||||
|
share: currentShare(),
|
||||||
path: path,
|
path: path,
|
||||||
last_update: null,
|
last_update: null,
|
||||||
last_access: null,
|
last_access: null,
|
||||||
@ -173,7 +180,7 @@ class FileSystem{
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if(err.code === 'BINARY_FILE') return Promise.reject(err);
|
if(err.code === 'BINARY_FILE') return Promise.reject(err);
|
||||||
|
|
||||||
return cache.update(cache.FILE_CONTENT, path, (response) => {
|
return cache.update(cache.FILE_CONTENT, [currentShare(), path], (response) => {
|
||||||
response.last_access = new Date();
|
response.last_access = new Date();
|
||||||
response.access_count += 1;
|
response.access_count += 1;
|
||||||
return response;
|
return response;
|
||||||
@ -184,12 +191,12 @@ class FileSystem{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
url(path){
|
url(path){
|
||||||
const url = '/api/files/cat?path='+prepare(path);
|
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||||
return Promise.resolve(url);
|
return Promise.resolve(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
save(path, file){
|
save(path, file){
|
||||||
const url = '/api/files/cat?path='+prepare(path);
|
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||||
let formData = new window.FormData();
|
let formData = new window.FormData();
|
||||||
formData.append('file', file, "test");
|
formData.append('file', file, "test");
|
||||||
return this._replace(path, 'loading')
|
return this._replace(path, 'loading')
|
||||||
@ -207,7 +214,7 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
mkdir(path, step){
|
mkdir(path, step){
|
||||||
const url = '/api/files/mkdir?path='+prepare(path),
|
const url = appendShareToUrl('/api/files/mkdir?path='+prepare(path)),
|
||||||
origin_path = pathBuilder(this.current_path, basename(path), 'directoy'),
|
origin_path = pathBuilder(this.current_path, basename(path), 'directoy'),
|
||||||
destination_path = path;
|
destination_path = path;
|
||||||
|
|
||||||
@ -240,8 +247,9 @@ class FileSystem{
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return this._replace(destination_path, null, 'loading')
|
return this._replace(destination_path, null, 'loading')
|
||||||
.then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve())
|
.then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve())
|
||||||
.then(() => cache.add(cache.FILE_PATH, destination_path, {
|
.then(() => cache.add(cache.FILE_PATH, [currentShare(), destination_path], {
|
||||||
path: destination_path,
|
path: destination_path,
|
||||||
|
share: currentShare(),
|
||||||
results: [],
|
results: [],
|
||||||
access_count: 0,
|
access_count: 0,
|
||||||
last_access: null,
|
last_access: null,
|
||||||
@ -310,12 +318,12 @@ class FileSystem{
|
|||||||
|
|
||||||
function query(){
|
function query(){
|
||||||
if(file){
|
if(file){
|
||||||
const url = '/api/files/cat?path='+prepare(path);
|
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||||
let formData = new window.FormData();
|
let formData = new window.FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return http_post(url, formData, 'multipart');
|
return http_post(url, formData, 'multipart');
|
||||||
}else{
|
}else{
|
||||||
const url = '/api/files/touch?path='+prepare(path);
|
const url = appendShareToUrl('/api/files/touch?path='+prepare(path));
|
||||||
return http_get(url);
|
return http_get(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,7 +339,7 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
mv(from, to){
|
mv(from, to){
|
||||||
const url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to),
|
const url = appendShareToUrl('/api/files/mv?from='+prepare(from)+"&to="+prepare(to)),
|
||||||
origin_path = from,
|
origin_path = from,
|
||||||
destination_path = to;
|
destination_path = to;
|
||||||
|
|
||||||
@ -344,11 +352,11 @@ class FileSystem{
|
|||||||
.then(() => this._replace(destination_path, null, 'loading'))
|
.then(() => this._replace(destination_path, null, 'loading'))
|
||||||
.then(() => this._refresh(origin_path, destination_path))
|
.then(() => this._refresh(origin_path, destination_path))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cache.update(cache.FILE_PATH, origin_path, (data) => {
|
cache.update(cache.FILE_PATH, [currentShare(), origin_path], (data) => {
|
||||||
data.path = data.path.replace(origin_path, destination_path);
|
data.path = data.path.replace(origin_path, destination_path);
|
||||||
return data;
|
return data;
|
||||||
}, false);
|
}, false);
|
||||||
cache.update(cache.FILE_CONTENT, origin_path, (data) => {
|
cache.update(cache.FILE_CONTENT, [currentShare(), origin_path], (data) => {
|
||||||
data.path = data.path.replace(origin_path, destination_path);
|
data.path = data.path.replace(origin_path, destination_path);
|
||||||
return data;
|
return data;
|
||||||
}, false);
|
}, false);
|
||||||
@ -369,7 +377,7 @@ class FileSystem{
|
|||||||
if(value.access_count >= 1 && value.path !== "/"){
|
if(value.access_count >= 1 && value.path !== "/"){
|
||||||
data.push(value);
|
data.push(value);
|
||||||
}
|
}
|
||||||
}).then(() => {
|
}, cache.FILE_PATH, [currentShare(), "/"]).then(() => {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
data
|
data
|
||||||
.sort((a,b) => a.access_count > b.access_count? -1 : 1)
|
.sort((a,b) => a.access_count > b.access_count? -1 : 1)
|
||||||
@ -389,8 +397,9 @@ class FileSystem{
|
|||||||
});
|
});
|
||||||
|
|
||||||
function update_cache(result){
|
function update_cache(result){
|
||||||
return cache.upsert(cache.FILE_CONTENT, path, (response) => {
|
return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => {
|
||||||
if(!response) response = {
|
if(!response) response = {
|
||||||
|
share: currentShare(),
|
||||||
path: path,
|
path: path,
|
||||||
last_access: null,
|
last_access: null,
|
||||||
last_update: null,
|
last_update: null,
|
||||||
@ -413,7 +422,7 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
_replace(path, icon, icon_previous){
|
_replace(path, icon, icon_previous){
|
||||||
return cache.update(cache.FILE_PATH, dirname(path), function(res){
|
return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
|
||||||
res.results = res.results.map((file) => {
|
res.results = res.results.map((file) => {
|
||||||
if(file.name === basename(path) && file.icon == icon_previous){
|
if(file.name === basename(path) && file.icon == icon_previous){
|
||||||
if(!icon){ delete file.icon; }
|
if(!icon){ delete file.icon; }
|
||||||
@ -425,7 +434,7 @@ class FileSystem{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_add(path, icon){
|
_add(path, icon){
|
||||||
return cache.upsert(cache.FILE_PATH, dirname(path), function(res){
|
return cache.upsert(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
|
||||||
if(!res || !res.results){
|
if(!res || !res.results){
|
||||||
res = {
|
res = {
|
||||||
path: dirname(path),
|
path: dirname(path),
|
||||||
@ -445,7 +454,7 @@ class FileSystem{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_remove(path, previous_icon){
|
_remove(path, previous_icon){
|
||||||
return cache.update(cache.FILE_PATH, dirname(path), function(res){
|
return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
|
||||||
if(!res) return null;
|
if(!res) return null;
|
||||||
res.results = res.results.filter((file) => {
|
res.results = res.results.filter((file) => {
|
||||||
return file.name === basename(path) && file.icon == previous_icon ? false : true;
|
return file.name === basename(path) && file.icon == previous_icon ? false : true;
|
||||||
@ -461,5 +470,4 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const Files = new FileSystem();
|
export const Files = new FileSystem();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { http_get, http_post, http_delete } from '../helpers/';
|
import { http_get, http_post, http_delete, appendShareToUrl } from '../helpers/';
|
||||||
|
|
||||||
class ShareModel {
|
class ShareModel {
|
||||||
constructor(){}
|
constructor(){}
|
||||||
@ -25,19 +25,20 @@ class ShareModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upsert(obj){
|
upsert(obj){
|
||||||
const url = `/api/share/${obj.id}`
|
const url = appendShareToUrl(`/api/share/${obj.id}`)
|
||||||
const data = Object.assign({}, obj);
|
const data = Object.assign({}, obj);
|
||||||
delete data.role;
|
delete data.role;
|
||||||
return http_post(url, data);
|
return http_post(url, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id){
|
remove(id){
|
||||||
const url = `/api/share/${id}`;
|
const url = appendShareToUrl(`/api/share/${id}`);
|
||||||
return http_delete(url);
|
return http_delete(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
proof(id, data){
|
proof(id, data){
|
||||||
// TODO
|
const url = `/api/share/${id}/proof`;
|
||||||
|
return http_post(url, data).then((res) => res.result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,15 +5,17 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
h1{margin: 5px 0; font-size: 3.1em;}
|
h1{margin: 5px 0; font-size: 3.1em;}
|
||||||
h2{margin: 10px 0; font-weight: normal; opacity: 0.9;}
|
h2{margin: 10px 0; font-weight: normal; opacity: 0.9; font-weight: 100;}
|
||||||
p{font-style: italic;}
|
p{font-style: italic;}
|
||||||
a{border-bottom: 1px dashed;}
|
a{border-bottom: 1px dashed;}
|
||||||
}
|
}
|
||||||
|
|
||||||
.backnav {
|
.backnav {
|
||||||
|
font-weight: 100;
|
||||||
.component_icon {
|
.component_icon {
|
||||||
height: 25px;
|
height: 23px;
|
||||||
margin-right: -2px;
|
margin-right: -3px;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Files } from '../model/';
|
import { Files } from '../model/';
|
||||||
import { notify, alert } from '../helpers/';
|
import { notify, alert, currentShare } from '../helpers/';
|
||||||
import Path from 'path';
|
import Path from 'path';
|
||||||
import Worker from "../worker/search.worker.js";
|
import Worker from "../worker/search.worker.js";
|
||||||
import { Observable } from "rxjs/Observable";
|
import { Observable } from "rxjs/Observable";
|
||||||
@ -371,11 +371,12 @@ export const onUpload = function(path, e){
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const worker = new Worker();9
|
const worker = new Worker();
|
||||||
export const onSearch = (keyword, path = "/") => {
|
export const onSearch = (keyword, path = "/") => {
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
action: "search::find",
|
action: "search::find",
|
||||||
path: path,
|
path: path,
|
||||||
|
share: currentShare(),
|
||||||
keyword: keyword
|
keyword: keyword
|
||||||
});
|
});
|
||||||
return new Observable((obs) => {
|
return new Observable((obs) => {
|
||||||
|
|||||||
@ -244,6 +244,9 @@ export class FilesPage extends React.Component {
|
|||||||
<Loader/>
|
<Loader/>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sidebar close">
|
||||||
|
THIS IS A MENUBAR
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -25,6 +25,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar{
|
||||||
|
width: 250px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
background: var(--light);
|
||||||
|
&.close{width: 0;}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-y{
|
.scroll-y{
|
||||||
|
|||||||
@ -17,11 +17,12 @@ export class FrequentlyAccess extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<ReactCSSTransitionGroup transitionName="frequent-access" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={300}>
|
<ReactCSSTransitionGroup transitionName="frequent-access" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={300}>
|
||||||
<Container>
|
<Container>
|
||||||
|
<span>Quick Access</span>
|
||||||
<div className="component_frequently-access">
|
<div className="component_frequently-access">
|
||||||
{
|
{
|
||||||
this.props.files.map(function(path, index){
|
this.props.files.map(function(path, index){
|
||||||
return (
|
return (
|
||||||
<Link key={path} to={"/files"+path}>
|
<Link key={path} to={"/files"+path+window.location.search}>
|
||||||
<Icon name={'directory'} />
|
<Icon name={'directory'} />
|
||||||
<div>{Path.basename(path)}</div>
|
<div>{Path.basename(path)}</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
a{
|
a{
|
||||||
width: 33.33%;
|
width: 33.33%;
|
||||||
background: var(--bg-color);
|
background: var(--light-color);
|
||||||
box-shadow: rgba(158, 163, 172, 0.3) 0px 19px 60px, rgba(158, 163, 172, 0.22) 0px 15px 20px;
|
box-shadow: rgba(158, 163, 172, 0.3) 0px 19px 60px, rgba(158, 163, 172, 0.22) 0px 15px 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { NgIf, Icon } from '../../components/';
|
import { NgIf, Icon } from '../../components/';
|
||||||
import { Share } from '../../model/';
|
import { Share } from '../../model/';
|
||||||
import { randomString, notify, absoluteToRelative } from '../../helpers/';
|
import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/';
|
||||||
import './share.scss';
|
import './share.scss';
|
||||||
|
|
||||||
export class ShareComponent extends React.Component {
|
export class ShareComponent extends React.Component {
|
||||||
@ -32,14 +32,20 @@ export class ShareComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
Share.all(this.props.path)
|
Share.all(this.props.path).then((existings) => {
|
||||||
.then((existings) => {
|
this.refreshModal();
|
||||||
this.refreshModal();
|
this.setState({
|
||||||
this.setState({existings: existings});
|
existings: existings.sort((a, b) => {
|
||||||
|
return a.path.split("/").length > b.path.split("/").length;
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState(key, value){
|
updateState(key, value){
|
||||||
|
if(key === "role"){
|
||||||
|
this.setState(this.resetState());
|
||||||
|
}
|
||||||
if(this.state[key] === value){
|
if(this.state[key] === value){
|
||||||
this.setState({[key]: null});
|
this.setState({[key]: null});
|
||||||
}else{
|
}else{
|
||||||
@ -85,10 +91,13 @@ export class ShareComponent extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onRegisterLink(e){
|
copyLinkInClipboard(link){
|
||||||
this.refs.$input.select();
|
copyToClipboard(link);
|
||||||
document.execCommand("copy");
|
|
||||||
notify.send("The link was copied in the clipboard", "INFO");
|
notify.send("The link was copied in the clipboard", "INFO");
|
||||||
|
}
|
||||||
|
|
||||||
|
onRegisterLink(e){
|
||||||
|
this.copyLinkInClipboard(this.refs.$input.value);
|
||||||
|
|
||||||
const link = {
|
const link = {
|
||||||
role: this.state.role,
|
role: this.state.role,
|
||||||
@ -137,9 +146,9 @@ export class ShareComponent extends React.Component {
|
|||||||
return Share.upsert(link)
|
return Share.upsert(link)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if(this.state.url !== null && this.state.url !== this.state.id){
|
if(this.state.url !== null && this.state.url !== this.state.id){
|
||||||
this.onDeleteLink(this.state.id)
|
this.onDeleteLink(this.state.id);
|
||||||
}
|
}
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
})
|
})
|
||||||
.then(() => this.setState(this.resetState()))
|
.then(() => this.setState(this.resetState()))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -152,11 +161,13 @@ export class ShareComponent extends React.Component {
|
|||||||
|
|
||||||
render(){
|
render(){
|
||||||
const beautifulPath = function(from, to){
|
const beautifulPath = function(from, to){
|
||||||
return to;
|
if(filetype(from) === "directory"){
|
||||||
const p = absoluteToRelative(from, to);
|
from = from.split("/");
|
||||||
if(p === "./"){
|
from = from.slice(0, from.length - 1);
|
||||||
return "Current folder";
|
from = from.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let p = absoluteToRelative(from, to);
|
||||||
return p.length < to.length ? p : to;
|
return p.length < to.length ? p : to;
|
||||||
};
|
};
|
||||||
const urlify = function(str){
|
const urlify = function(str){
|
||||||
@ -197,8 +208,10 @@ export class ShareComponent extends React.Component {
|
|||||||
this.state.existings && this.state.existings.map((link, i) => {
|
this.state.existings && this.state.existings.map((link, i) => {
|
||||||
return (
|
return (
|
||||||
<div className="link-details" key={i}>
|
<div className="link-details" key={i}>
|
||||||
<span className="role">{link.role}</span>
|
<span onClick={this.copyLinkInClipboard.bind(this, window.location.origin+"/s/"+link.id)} className="copy role">
|
||||||
<span className="path">{beautifulPath(this.props.path, link.path)}</span>
|
{link.role}
|
||||||
|
</span>
|
||||||
|
<span onClick={this.copyLinkInClipboard.bind(this, window.location.origin+"/s/"+link.id)} className="copy path">{beautifulPath(this.props.path, link.path)}</span>
|
||||||
<Icon onClick={this.onDeleteLink.bind(this, link.id)} name="delete"/>
|
<Icon onClick={this.onDeleteLink.bind(this, link.id)} name="delete"/>
|
||||||
<Icon onClick={this.onLoad.bind(this, link)} name="edit"/>
|
<Icon onClick={this.onLoad.bind(this, link)} name="edit"/>
|
||||||
</div>
|
</div>
|
||||||
@ -221,16 +234,18 @@ export class ShareComponent extends React.Component {
|
|||||||
<NgIf type="inline" cond={!this.state.show_advanced}><Icon name="arrow_bottom"/></NgIf>
|
<NgIf type="inline" cond={!this.state.show_advanced}><Icon name="arrow_bottom"/></NgIf>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="share--content advanced-settings no-select">
|
<div className="share--content advanced-settings no-select">
|
||||||
<NgIf cond={this.state.show_advanced === true}>
|
<NgIf cond={false}>
|
||||||
<SuperCheckbox value={this.state.can_manage_own} label="Can Manage Own" onChange={this.updateState.bind(this, 'can_manage_own')}/>
|
<SuperCheckbox value={this.state.can_manage_own} label="Can Manage Own" onChange={this.updateState.bind(this, 'can_manage_own')}/>
|
||||||
<SuperCheckbox value={this.state.can_share} label="Can Share" onChange={this.updateState.bind(this, 'can_share')}/>
|
<SuperCheckbox value={this.state.can_share} label="Can Share" onChange={this.updateState.bind(this, 'can_share')}/>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.show_advanced === true}>
|
||||||
<SuperCheckbox value={datify(this.state.expire)} label="Expiration" placeholder="The link won't be valid after" onChange={this.updateState.bind(this, 'expire')} inputType="date"/>
|
<SuperCheckbox value={datify(this.state.expire)} label="Expiration" placeholder="The link won't be valid after" onChange={this.updateState.bind(this, 'expire')} inputType="date"/>
|
||||||
<SuperCheckbox value={this.state.url} label="Custom Link url" placeholder="beautiful_url" onChange={(val) => this.updateState('url', urlify(val))} inputType="text"/>
|
<SuperCheckbox value={this.state.url} label="Custom Link url" placeholder="beautiful_url" onChange={(val) => this.updateState('url', urlify(val))} inputType="text"/>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shared-link">
|
<div className="shared-link">
|
||||||
<input ref="$input" onClick={this.onRegisterLink.bind(this)} type="text" value={window.location.origin+"/s/"+(this.state.url || this.state.id)} onChange={() => {}}/>
|
<input ref="$input" className="copy" onClick={this.onRegisterLink.bind(this)} type="text" value={window.location.origin+"/s/"+(this.state.url || this.state.id)} onChange={() => {}}/>
|
||||||
</div>
|
</div>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
</div>
|
</div>
|
||||||
@ -240,15 +255,18 @@ export class ShareComponent extends React.Component {
|
|||||||
|
|
||||||
const SuperCheckbox = (props) => {
|
const SuperCheckbox = (props) => {
|
||||||
const onCheckboxTick = (e) => {
|
const onCheckboxTick = (e) => {
|
||||||
|
if(props.inputType === undefined){
|
||||||
|
return props.onChange(e.target.checked ? true : false);
|
||||||
|
}
|
||||||
return props.onChange(e.target.checked ? "" : null);
|
return props.onChange(e.target.checked ? "" : null);
|
||||||
};
|
};
|
||||||
const onValueChange = (e) => {
|
const onValueChange = (e) => {
|
||||||
props.onChange(e.target.value);
|
props.onChange(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _is_expended = function(val){
|
const _is_expended = function(val){
|
||||||
return val === null || val === undefined ? false : true;
|
return val === null || val === undefined || val === false ? false : true;
|
||||||
}(props.value);
|
}(props.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="component_supercheckbox">
|
<div className="component_supercheckbox">
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@ -9,6 +9,9 @@
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.copy {
|
||||||
|
cursor: copy;
|
||||||
|
}
|
||||||
.share--content{
|
.share--content{
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
@ -53,7 +56,7 @@
|
|||||||
.path{
|
.path{
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
max-width: 180px;
|
max-width: 170px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { Card, NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList,
|
|||||||
import { pathBuilder, debounce } from '../../helpers/';
|
import { pathBuilder, debounce } from '../../helpers/';
|
||||||
import "./submenu.scss";
|
import "./submenu.scss";
|
||||||
|
|
||||||
|
let SEARCH_KEYWORD = "";
|
||||||
|
|
||||||
@EventEmitter
|
@EventEmitter
|
||||||
export class Submenu extends React.Component {
|
export class Submenu extends React.Component {
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -14,6 +16,12 @@ export class Submenu extends React.Component {
|
|||||||
search_input_visible: false,
|
search_input_visible: false,
|
||||||
search_keyword: ""
|
search_keyword: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(SEARCH_KEYWORD){
|
||||||
|
this.state.search_input_visible = true;
|
||||||
|
this.state.search_keyword = SEARCH_KEYWORD;
|
||||||
|
}
|
||||||
|
|
||||||
this.onSearchChange_Backpressure = debounce(this.onSearchChange, 400);
|
this.onSearchChange_Backpressure = debounce(this.onSearchChange, 400);
|
||||||
this._onEscapeKeyPress = (e) => {
|
this._onEscapeKeyPress = (e) => {
|
||||||
if(e.keyCode === 27){ // escape key
|
if(e.keyCode === 27){ // escape key
|
||||||
@ -38,8 +46,12 @@ export class Submenu extends React.Component {
|
|||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
window.addEventListener('keydown', this._onEscapeKeyPress);
|
window.addEventListener('keydown', this._onEscapeKeyPress);
|
||||||
|
if(this.state.search_input_visible === true){
|
||||||
|
this.onSearchChange(this.state.search_keyword);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
componentWillUnmount(){
|
componentWillUnmount(){
|
||||||
|
SEARCH_KEYWORD = this.state.search_keyword;
|
||||||
window.removeEventListener('keydown', this._onEscapeKeyPress);
|
window.removeEventListener('keydown', this._onEscapeKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -214,12 +214,12 @@ export class ExistingThing extends React.Component {
|
|||||||
|
|
||||||
return connectDragSource(connectDropNativeFile(connectDropFile(
|
return connectDragSource(connectDropNativeFile(connectDropFile(
|
||||||
<div className={"component_thing view-"+this.props.view}>
|
<div className={"component_thing view-"+this.props.view}>
|
||||||
<Link to={this.props.file.link + location.search || "#"}>
|
<Link to={this.props.file.link + location.search}>
|
||||||
<Card ref="$card"className={this.state.hover} className={className}>
|
<Card ref="$card"className={this.state.hover} className={className}>
|
||||||
<Image preview={this.state.preview} icon={this.props.file.icon || this.props.file.type} view={this.props.view} path={path.join(this.props.path, this.props.file.name)} />
|
<Image preview={this.state.preview} icon={this.props.file.icon || this.props.file.type} view={this.props.view} path={path.join(this.props.path, this.props.file.name)} />
|
||||||
<Filename filename={this.props.file.name} filesize={this.props.file.size} filetype={this.props.file.type} onRename={this.onRename.bind(this)} is_renaming={this.state.is_renaming} onRenameCancel={this.onRenameRequest.bind(this, false)}/>
|
<Filename filename={this.props.file.name} filesize={this.props.file.size} filetype={this.props.file.type} onRename={this.onRename.bind(this)} is_renaming={this.state.is_renaming} onRenameCancel={this.onRenameRequest.bind(this, false)}/>
|
||||||
<DateTime show={this.state.icon !== 'loading'} timestamp={this.props.file.time} />
|
<DateTime show={this.state.icon !== 'loading'} timestamp={this.props.file.time} />
|
||||||
<ActionButton onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} onClickShare={this.onShareRequest.bind(this)} is_renaming={this.state.is_renaming} can_rename={this.props.metadata.can_rename !== false} can_delete={this.props.metadata.can_delete !== false} />
|
<ActionButton onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} onClickShare={this.onShareRequest.bind(this)} is_renaming={this.state.is_renaming} can_rename={this.props.metadata.can_rename !== false} can_delete={this.props.metadata.can_delete !== false} can_share={this.props.metadata.can_share !== false} />
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -310,7 +310,7 @@ const ActionButton = (props) => {
|
|||||||
<NgIf cond={props.can_delete !== false} type="inline">
|
<NgIf cond={props.can_delete !== false} type="inline">
|
||||||
<Icon name="delete" onClick={onDelete} className="component_updater--icon"/>
|
<Icon name="delete" onClick={onDelete} className="component_updater--icon"/>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
<NgIf cond={false && props.can_share !== false} type="inline">
|
<NgIf cond={props.can_share !== false} type="inline">
|
||||||
<Icon name="share" onClick={onShare} className="component_updater--icon"/>
|
<Icon name="share" onClick={onShare} className="component_updater--icon"/>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,40 +2,60 @@ import React from 'react';
|
|||||||
import { Redirect } from 'react-router';
|
import { Redirect } from 'react-router';
|
||||||
|
|
||||||
import { Share } from '../model/';
|
import { Share } from '../model/';
|
||||||
import { notify } from '../helpers/';
|
import { notify, basename, filetype } from '../helpers/';
|
||||||
import { Loader, Input, Button, Container } from '../components/';
|
import { Loader, Input, Button, Container, ErrorPage, Icon, NgIf } from '../components/';
|
||||||
import './error.scss';
|
import './error.scss';
|
||||||
|
import './sharepage.scss';
|
||||||
|
|
||||||
|
@ErrorPage
|
||||||
export class SharePage extends React.Component {
|
export class SharePage extends React.Component {
|
||||||
constructor(props){
|
constructor(props){
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
redirection: null,
|
path: null,
|
||||||
loading: true,
|
key: null,
|
||||||
request_password: false,
|
error: null,
|
||||||
request_username: false
|
loading: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
Share.get(this.props.match.params.id)
|
this._proofQuery(this.props.match.params.id).then(() => {
|
||||||
.then((res) => {
|
if(this.refs.$input) {
|
||||||
console.log(res);
|
this.refs.$input.ref.focus();
|
||||||
this.setState({
|
}
|
||||||
loading: false,
|
});
|
||||||
request_password: true
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((res) => {
|
|
||||||
this.setState({
|
|
||||||
loading: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submitProof(e, type, value){
|
submitProof(e, type, value){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log(type, value);
|
this.setState({loading: true});
|
||||||
|
this._proofQuery(this.props.match.params.id, {type: type, value:value});
|
||||||
|
}
|
||||||
|
|
||||||
|
_proofQuery(id, data = {}){
|
||||||
|
this.setState({loading: true});
|
||||||
|
return Share.proof(id, data).then((res) => {
|
||||||
|
if(this.refs.$input) {
|
||||||
|
this.refs.$input.ref.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let st = {
|
||||||
|
key: res.key,
|
||||||
|
path: res.path || null,
|
||||||
|
share: res.id,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
if(res.message){
|
||||||
|
notify.send(res.message, "info");
|
||||||
|
}else if(res.error){
|
||||||
|
st.error = res.error;
|
||||||
|
window.setTimeout(() => this.setState({error: null}), 500);
|
||||||
|
}
|
||||||
|
return new Promise((done) => {
|
||||||
|
this.setState(st, () => done());
|
||||||
|
});
|
||||||
|
}).catch((err) => this.props.error(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -45,52 +65,60 @@ export class SharePage extends React.Component {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if(this.state.loading === true){
|
let className = this.state.error ? "error rand-"+Math.random().toString() : "";
|
||||||
return ( <div> <Loader /> </div> );
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.request_password === true){
|
if(this.state.path !== null){
|
||||||
return (
|
if(filetype(this.state.path) === "directory"){
|
||||||
<Container maxWidth="350px">
|
return ( <Redirect to={`/files/?share=${this.state.share}`} /> );
|
||||||
<form onSubmit={(e) => this.submitProof(e, "password", this.refs.$input.ref.value)} style={marginTop()}>
|
|
||||||
<Input ref="$input" type="password" placeholder="Password" />
|
|
||||||
<Button theme="emphasis">OK</Button>
|
|
||||||
</form>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}else if(this.state.request_username === true){
|
|
||||||
return (
|
|
||||||
<Container maxWidth="350px">
|
|
||||||
<form onSubmit={(e) => this.submitProof(e, "email", this.refs.$input.ref.value)} style={marginTop()}>
|
|
||||||
<Input ref="$input" type="text" placeholder="Your email address" />
|
|
||||||
<Button theme="emphasis">OK</Button>
|
|
||||||
</form>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}else if(this.state.request_verification === true){
|
|
||||||
return (
|
|
||||||
<Container maxWidth="350px">
|
|
||||||
<form onSubmit={(e) => this.submitProof(e, "code", this.refs.$input.ref.value)} style={marginTop()}>
|
|
||||||
<Input ref="$input" type="text" placeholder="Code" />
|
|
||||||
<Button theme="emphasis">OK</Button>
|
|
||||||
</form>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.redirection !== null){
|
|
||||||
if(this.state.redirection.slice(-1) === "/"){
|
|
||||||
return ( <Redirect to={"/files" + this.state.redirection} /> );
|
|
||||||
}else{
|
}else{
|
||||||
return ( <Redirect to={"/view" + this.state.redirection} /> );
|
return ( <Redirect to={`/view/${basename(this.state.path)}?share=${this.state.share}`} /> );
|
||||||
}
|
}
|
||||||
}else{
|
} else if (this.state.key === null){
|
||||||
return (
|
return (
|
||||||
<div className="error-page">
|
<div style={marginTop()}>
|
||||||
<h1>Oops!</h1>
|
<Loader />
|
||||||
<h2>There's nothing in here</h2>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if(this.state.key === "code"){
|
||||||
|
return (
|
||||||
|
<Container maxWidth="300px" className="sharepage_component">
|
||||||
|
<form className={className} onSubmit={(e) => this.submitProof(e, "code", this.refs.$input.ref.value)} style={marginTop()}>
|
||||||
|
<Input ref="$input" type="text" placeholder="Code" />
|
||||||
|
<Button theme="transparent">
|
||||||
|
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else if(this.state.key === "password"){
|
||||||
|
return (
|
||||||
|
<Container maxWidth="300px" className="sharepage_component">
|
||||||
|
<form className={className} onSubmit={(e) => this.submitProof(e, "password", this.refs.$input.ref.value)} style={marginTop()}>
|
||||||
|
<Input ref="$input" type="password" placeholder="Password" />
|
||||||
|
<Button theme="transparent">
|
||||||
|
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}else if(this.state.key === "email"){
|
||||||
|
return (
|
||||||
|
<Container maxWidth="300px" className="sharepage_component">
|
||||||
|
<form className={className} onSubmit={(e) => this.submitProof(e, "email", this.refs.$input.ref.value)} style={marginTop()}>
|
||||||
|
<Input ref="$input" type="text" placeholder="Your email address" />
|
||||||
|
<Button theme="transparent">
|
||||||
|
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="error-page">
|
||||||
|
<h1>Oops!</h1>
|
||||||
|
<h2>There's nothing in here</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
client/pages/sharepage.scss
Normal file
56
client/pages/sharepage.scss
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
.sharepage_component {
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
background: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 2px 2px 2px rgba(0,0,0,0.05);
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: inherit;
|
||||||
|
padding: 0 10px;
|
||||||
|
.component_icon {
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error{
|
||||||
|
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
animation: 0.2s ease-out 0s 1 enterZoomIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes enterZoomIn {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.1)
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
10%, 90% {
|
||||||
|
transform: translate3d(-1px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20%, 80% {
|
||||||
|
transform: translate3d(2px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
30%, 50%, 70% {
|
||||||
|
transform: translate3d(-4px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
40%, 60% {
|
||||||
|
transform: translate3d(4px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -309,7 +309,7 @@ class OrgViewer extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</StickyContainer>
|
</StickyContainer>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Link, withRouter } from 'react-router-dom';
|
|||||||
import { Files } from '../../model/';
|
import { Files } from '../../model/';
|
||||||
import { sort } from '../../pages/filespage.helper.js';
|
import { sort } from '../../pages/filespage.helper.js';
|
||||||
import { Icon, NgIf, EventReceiver, EventEmitter } from '../../components/';
|
import { Icon, NgIf, EventReceiver, EventEmitter } from '../../components/';
|
||||||
import { dirname, basename, settings_get, getMimeType, debounce, gid } from '../../helpers/';
|
import { dirname, basename, settings_get, getMimeType, debounce, gid, appendShareToUrl } from '../../helpers/';
|
||||||
import './pager.scss';
|
import './pager.scss';
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +25,6 @@ export class Pager extends React.Component {
|
|||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
this.setNavigation(this.props);
|
this.setNavigation(this.props);
|
||||||
window.addEventListener("keyup", this.navigate);
|
window.addEventListener("keyup", this.navigate);
|
||||||
|
|
||||||
this.props.subscribe('media::next', () => {
|
this.props.subscribe('media::next', () => {
|
||||||
this.navigatePage(this.calculateNextPageNumber(this.state.n));
|
this.navigatePage(this.calculateNextPageNumber(this.state.n));
|
||||||
});
|
});
|
||||||
@ -35,7 +34,7 @@ export class Pager extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props){
|
componentWillReceiveProps(props){
|
||||||
if(props.path === this.props.path){
|
if(props.path !== this.props.path){
|
||||||
this.setNavigation(props);
|
this.setNavigation(props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +56,8 @@ export class Pager extends React.Component {
|
|||||||
|
|
||||||
navigatePage(n){
|
navigatePage(n){
|
||||||
if(this.state.files[n]){
|
if(this.state.files[n]){
|
||||||
this.props.history.push(this.state.files[n].link+"?once="+gid());
|
const url = appendShareToUrl(this.state.files[n].link)
|
||||||
|
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){
|
if(!this.state.files[preload_index].path){
|
||||||
@ -79,6 +79,10 @@ export class Pager extends React.Component {
|
|||||||
|
|
||||||
setNavigation(props){
|
setNavigation(props){
|
||||||
Files._ls_from_cache(dirname(props.path))
|
Files._ls_from_cache(dirname(props.path))
|
||||||
|
.then((f) => {
|
||||||
|
if(f === null) return Promise.reject({code: "NO_DATA"});
|
||||||
|
return Promise.resolve(f)
|
||||||
|
})
|
||||||
.then((f) => f.results.filter((file) => (isImage(file.name) || isVideo(file.name)) && file.type === "file"))
|
.then((f) => f.results.filter((file) => (isImage(file.name) || isVideo(file.name)) && file.type === "file"))
|
||||||
.then((f) => sort(f, settings_get('filespage_sort') || 'type'))
|
.then((f) => sort(f, settings_get('filespage_sort') || 'type'))
|
||||||
.then((f) => findPosition(f, basename(props.path)))
|
.then((f) => findPosition(f, basename(props.path)))
|
||||||
@ -87,7 +91,8 @@ export class Pager extends React.Component {
|
|||||||
files: res[0],
|
files: res[0],
|
||||||
n: res[1]
|
n: res[1]
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
const findPosition = (files, filename) => {
|
const findPosition = (files, filename) => {
|
||||||
let i;
|
let i;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ self.onmessage = function(message){
|
|||||||
if(current_search != null){
|
if(current_search != null){
|
||||||
current_search.unsubscribe();
|
current_search.unsubscribe();
|
||||||
}
|
}
|
||||||
current_search = Search(message.data.path, message.data.keyword).subscribe((a) => {
|
current_search = Search([message.data.share, message.data.path], message.data.keyword).subscribe((a) => {
|
||||||
self.postMessage({type: "search::found", files: a});
|
self.postMessage({type: "search::found", files: a});
|
||||||
}, null, () => {
|
}, null, () => {
|
||||||
self.postMessage({type: "search::completed"})
|
self.postMessage({type: "search::completed"})
|
||||||
@ -16,7 +16,7 @@ self.onmessage = function(message){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Search(path, keyword){
|
function Search(key, keyword){
|
||||||
let results = [];
|
let results = [];
|
||||||
return new Observable((obs) => {
|
return new Observable((obs) => {
|
||||||
obs.next(results);
|
obs.next(results);
|
||||||
@ -32,7 +32,7 @@ function Search(path, keyword){
|
|||||||
results = results.concat(found);
|
results = results.concat(found);
|
||||||
obs.next(results);
|
obs.next(results);
|
||||||
}
|
}
|
||||||
}, cache.FILE_PATH, path).then(() => {
|
}, cache.FILE_PATH, [key[0], key[1]]).then(() => {
|
||||||
obs.complete(results);
|
obs.complete(results);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,6 +15,11 @@
|
|||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"telemetry": true
|
"telemetry": true
|
||||||
},
|
},
|
||||||
|
"smtp": {
|
||||||
|
"addr": "smtp.gmail.com",
|
||||||
|
"username": "mickael.kerjean@gmail.com",
|
||||||
|
"password": "test"
|
||||||
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"gdrive": {
|
"gdrive": {
|
||||||
"client_id": "",
|
"client_id": "",
|
||||||
|
|||||||
@ -36,6 +36,13 @@ type Config struct {
|
|||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
Telemetry bool `json:"telemetry"`
|
Telemetry bool `json:"telemetry"`
|
||||||
} `json:"log"`
|
} `json:"log"`
|
||||||
|
Email struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
From string `json:"from"`
|
||||||
|
} `json:"email"`
|
||||||
OAuthProvider struct {
|
OAuthProvider struct {
|
||||||
Dropbox struct {
|
Dropbox struct {
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
const (
|
const (
|
||||||
COOKIE_NAME = "auth"
|
COOKIE_NAME_AUTH = "auth"
|
||||||
|
COOKIE_NAME_PROOF = "proof"
|
||||||
COOKIE_PATH = "/api/"
|
COOKIE_PATH = "/api/"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,58 +1,144 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
mathrand "math/rand"
|
||||||
|
"math/big"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Encrypt(keystr string, text map[string]string) (string, error) {
|
var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||||
key := []byte(keystr)
|
|
||||||
plaintext, err := json.Marshal(text)
|
|
||||||
if err != nil {
|
|
||||||
return "", NewError("json marshalling: "+err.Error(), 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
block, err := aes.NewCipher(key)
|
func EncryptString(secret string, json string) (string, error) {
|
||||||
|
d, err := compress([]byte(json))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", NewError("encryption issue (cipher): "+err.Error(), 500)
|
return "", err
|
||||||
}
|
}
|
||||||
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
d, err = encrypt([]byte(secret), d)
|
||||||
iv := ciphertext[:aes.BlockSize]
|
if err != nil {
|
||||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
return "", err
|
||||||
return "", NewError("encryption issue: "+err.Error(), 500)
|
|
||||||
}
|
}
|
||||||
stream := cipher.NewCFBEncrypter(block, iv)
|
return base64.URLEncoding.EncodeToString(d), nil
|
||||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
|
||||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Decrypt(keystr string, cryptoText string) (map[string]string, error) {
|
func DecryptString(secret string, data string) (string, error){
|
||||||
var raw map[string]string
|
d, err := base64.URLEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
key := []byte(keystr)
|
return "", err
|
||||||
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
|
|
||||||
if err != nil || len(ciphertext) < aes.BlockSize {
|
|
||||||
return raw, NewError("Cipher is too short", 500)
|
|
||||||
}
|
}
|
||||||
|
d, err = decrypt([]byte(secret), d)
|
||||||
iv := ciphertext[:aes.BlockSize]
|
if err != nil {
|
||||||
ciphertext = ciphertext[aes.BlockSize:]
|
return "", err
|
||||||
stream := cipher.NewCFBDecrypter(block, iv)
|
}
|
||||||
stream.XORKeyStream(ciphertext, ciphertext)
|
d, err = decompress(d)
|
||||||
|
if err != nil {
|
||||||
json.Unmarshal(ciphertext, &raw)
|
return "", err
|
||||||
return raw, nil
|
}
|
||||||
|
return string(d), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateID(params map[string]string) string {
|
func Hash(str string) string {
|
||||||
|
hasher := sha1.New()
|
||||||
|
hasher.Write([]byte(str))
|
||||||
|
return "sha1::" + base32.HexEncoding.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomString(n int) string {
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
max := *big.NewInt(int64(len(Letters)))
|
||||||
|
r, err := rand.Int(rand.Reader, &max)
|
||||||
|
if err != nil {
|
||||||
|
b[i] = Letters[0]
|
||||||
|
} else {
|
||||||
|
b[i] = Letters[r.Int64()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func QuickString(n int) string {
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = Letters[mathrand.Intn(len(Letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encrypt(key []byte, plaintext []byte) ([]byte, error) {
|
||||||
|
c, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrypt(key []byte, ciphertext []byte) ([]byte, error) {
|
||||||
|
c, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(ciphertext) < nonceSize {
|
||||||
|
return nil, NewError("ciphertext too short", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||||
|
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compress(something []byte) ([]byte, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := zlib.NewWriter(&b)
|
||||||
|
w.Write(something)
|
||||||
|
w.Close()
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompress(something []byte) ([]byte, error) {
|
||||||
|
b := bytes.NewBuffer(something)
|
||||||
|
r, err := zlib.NewReader(b)
|
||||||
|
if err != nil {
|
||||||
|
return []byte(""), nil
|
||||||
|
}
|
||||||
|
r.Close()
|
||||||
|
return ioutil.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(something []byte) ([]byte, error) {
|
||||||
|
return something, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verify(something []byte) ([]byte, error) {
|
||||||
|
return something, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateID(params map[string]string) string {
|
||||||
p := "type =>" + params["type"]
|
p := "type =>" + params["type"]
|
||||||
p += "host =>" + params["host"]
|
p += "host =>" + params["host"]
|
||||||
p += "hostname =>" + params["hostname"]
|
p += "hostname =>" + params["hostname"]
|
||||||
@ -63,7 +149,5 @@ func GenerateID(params map[string]string) string {
|
|||||||
p += "endpoint =>" + params["endpoint"]
|
p += "endpoint =>" + params["endpoint"]
|
||||||
p += "bearer =>" + params["bearer"]
|
p += "bearer =>" + params["bearer"]
|
||||||
p += "token =>" + params["token"]
|
p += "token =>" + params["token"]
|
||||||
hasher := sha1.New()
|
return Hash(p)
|
||||||
hasher.Write([]byte(p))
|
|
||||||
return "sha1::" + base32.HexEncoding.EncodeToString(hasher.Sum(nil))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,18 +5,18 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEncryptSomething(t *testing.T) {
|
func TestEncryptString(t *testing.T) {
|
||||||
key := "test|test|test|test|test"
|
key := "test|test|test|test|test"
|
||||||
|
text := "I'm some text"
|
||||||
d := make(map[string]string)
|
a, err := EncryptString(key, text)
|
||||||
d["foo"] = "bar"
|
|
||||||
|
|
||||||
str, err := Encrypt(key, d)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, a)
|
||||||
|
assert.NotEqual(t, a, text)
|
||||||
|
|
||||||
data, err := Decrypt(key, str)
|
b, err := DecryptString(key, a)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "bar", data["foo"])
|
assert.Equal(t, b, text)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIDGeneration(t *testing.T) {
|
func TestIDGeneration(t *testing.T) {
|
||||||
@ -32,3 +32,11 @@ func TestIDGeneration(t *testing.T) {
|
|||||||
assert.NotEqual(t, id1, id2)
|
assert.NotEqual(t, id1, id2)
|
||||||
assert.Equal(t, id2, id3)
|
assert.Equal(t, id2, id3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStringGeneration(t *testing.T) {
|
||||||
|
str := QuickString(10)
|
||||||
|
str1 := QuickString(10)
|
||||||
|
str2 := QuickString(10)
|
||||||
|
assert.Equal(t, len(str), 10)
|
||||||
|
t.Log(str, str1, str2)
|
||||||
|
}
|
||||||
|
|||||||
@ -56,5 +56,7 @@ type Metadata struct {
|
|||||||
CanRename *bool `json:"can_rename,omitempty"`
|
CanRename *bool `json:"can_rename,omitempty"`
|
||||||
CanMove *bool `json:"can_move,omitempty"`
|
CanMove *bool `json:"can_move,omitempty"`
|
||||||
CanUpload *bool `json:"can_upload,omitempty"`
|
CanUpload *bool `json:"can_upload,omitempty"`
|
||||||
|
CanDelete *bool `json:"can_delete,omitempty"`
|
||||||
|
CanShare *bool `json:"can_share,omitempty"`
|
||||||
Expire *time.Time `json:"-"`
|
Expire *time.Time `json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,5 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
|
||||||
|
|
||||||
func RandomString(n int) string {
|
|
||||||
b := make([]rune, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = Letters[rand.Intn(len(Letters))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBool(t bool) *bool {
|
func NewBool(t bool) *bool {
|
||||||
return &t
|
return &t
|
||||||
}
|
}
|
||||||
@ -35,13 +21,13 @@ func NewBoolFromInterface(val interface{}) bool {
|
|||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func NewIntpFromInterface(val interface{}) *int {
|
func NewInt64pFromInterface(val interface{}) *int64 {
|
||||||
switch val.(type) {
|
switch val.(type) {
|
||||||
case int:
|
case int64:
|
||||||
v := val.(int)
|
v := val.(int64)
|
||||||
return &v
|
return &v
|
||||||
case float64:
|
case float64:
|
||||||
v := int(val.(float64))
|
v := int64(val.(float64))
|
||||||
return &v
|
return &v
|
||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package ctrl
|
|||||||
import (
|
import (
|
||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
"github.com/mickael-kerjean/nuage/server/services"
|
"github.com/mickael-kerjean/nuage/server/services"
|
||||||
|
"github.com/mickael-kerjean/nuage/server/model"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -18,6 +19,16 @@ type FileInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
var files []FileInfo
|
||||||
|
if model.CanRead(&ctx) == false {
|
||||||
|
if model.CanUpload(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("Permission denied", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SendSuccessResults(res, files)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
@ -30,7 +41,6 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files := []FileInfo{}
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
f := FileInfo{
|
f := FileInfo{
|
||||||
Name: entry.Name(),
|
Name: entry.Name(),
|
||||||
@ -48,10 +58,28 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
files = append(files, f)
|
files = append(files, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
var perms *Metadata
|
var perms *Metadata = &Metadata{}
|
||||||
if obj, ok := ctx.Backend.(interface{ Meta(path string) *Metadata }); ok {
|
if obj, ok := ctx.Backend.(interface{ Meta(path string) *Metadata }); ok {
|
||||||
perms = obj.Meta(path)
|
perms = obj.Meta(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model.CanEdit(&ctx) == false {
|
||||||
|
perms.CanCreateFile = NewBool(false)
|
||||||
|
perms.CanCreateDirectory = NewBool(false)
|
||||||
|
perms.CanRename = NewBool(false)
|
||||||
|
perms.CanMove = NewBool(false)
|
||||||
|
perms.CanDelete = NewBool(false)
|
||||||
|
}
|
||||||
|
if model.CanUpload(&ctx) == false {
|
||||||
|
perms.CanCreateDirectory = NewBool(false)
|
||||||
|
perms.CanRename = NewBool(false)
|
||||||
|
perms.CanMove = NewBool(false)
|
||||||
|
perms.CanDelete = NewBool(false)
|
||||||
|
}
|
||||||
|
if model.CanShare(&ctx) == false {
|
||||||
|
perms.CanShare = NewBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
SendSuccessResultsWithMetadata(res, files, perms)
|
SendSuccessResultsWithMetadata(res, files, perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +90,10 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
})
|
})
|
||||||
|
if model.CanRead(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("Permission denied", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -87,6 +119,11 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FileSave(ctx App, res http.ResponseWriter, req *http.Request) {
|
func FileSave(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if model.CanEdit(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("Permission denied", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
@ -112,6 +149,11 @@ func FileSave(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FileMv(ctx App, res http.ResponseWriter, req *http.Request) {
|
func FileMv(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if model.CanEdit(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("Permission denied", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
from, err := pathBuilder(ctx, req.URL.Query().Get("from"))
|
from, err := pathBuilder(ctx, req.URL.Query().Get("from"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
@ -136,6 +178,11 @@ func FileMv(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FileRm(ctx App, res http.ResponseWriter, req *http.Request) {
|
func FileRm(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if model.CanEdit(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("Permission denied", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
@ -150,6 +197,11 @@ func FileRm(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) {
|
func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if model.CanUpload(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("Permission denied", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
@ -165,6 +217,11 @@ func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) {
|
func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if model.CanUpload(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("Permission denied", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package ctrl
|
package ctrl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/mickael-kerjean/mux"
|
"github.com/mickael-kerjean/mux"
|
||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
"github.com/mickael-kerjean/nuage/server/model"
|
"github.com/mickael-kerjean/nuage/server/model"
|
||||||
@ -17,7 +18,7 @@ func SessionGet(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
r := Session {
|
r := Session {
|
||||||
IsAuth: false,
|
IsAuth: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Backend == nil {
|
if ctx.Backend == nil {
|
||||||
SendSuccessResult(res, r)
|
SendSuccessResult(res, r)
|
||||||
return
|
return
|
||||||
@ -63,13 +64,19 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
obfuscate, err := Encrypt(ctx.Config.General.SecretKey, session)
|
s, err := json.Marshal(session);
|
||||||
|
if err != nil {
|
||||||
|
SendErrorResult(res, NewError(err.Error(), 500))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obfuscate, err := EncryptString(ctx.Config.General.SecretKey, string(s))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorResult(res, NewError(err.Error(), 500))
|
SendErrorResult(res, NewError(err.Error(), 500))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cookie := http.Cookie{
|
cookie := http.Cookie{
|
||||||
Name: COOKIE_NAME,
|
Name: COOKIE_NAME_AUTH,
|
||||||
Value: obfuscate,
|
Value: obfuscate,
|
||||||
MaxAge: 60 * 60 * 24 * 30,
|
MaxAge: 60 * 60 * 24 * 30,
|
||||||
Path: COOKIE_PATH,
|
Path: COOKIE_PATH,
|
||||||
@ -88,7 +95,7 @@ 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{
|
cookie := http.Cookie{
|
||||||
Name: COOKIE_NAME,
|
Name: COOKIE_NAME_AUTH,
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: COOKIE_PATH,
|
Path: COOKIE_PATH,
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
package ctrl
|
package ctrl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/mickael-kerjean/mux"
|
"github.com/mickael-kerjean/mux"
|
||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
"github.com/mickael-kerjean/nuage/server/model"
|
"github.com/mickael-kerjean/nuage/server/model"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ShareList(ctx App, res http.ResponseWriter, req *http.Request) {
|
func ShareList(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
@ -23,12 +26,64 @@ func ShareGet(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SendSuccessResult(res, s)
|
SendSuccessResult(res, struct{
|
||||||
|
Id string `json:"id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
Id: s.Id,
|
||||||
|
Path: s.Path,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
|
func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if model.CanShare(&ctx) == false {
|
||||||
|
SendErrorResult(res, NewError("No permission", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s := extractParams(req, &ctx)
|
s := extractParams(req, &ctx)
|
||||||
s.Path = NewStringFromInterface(ctx.Body["path"])
|
s.Path = NewStringFromInterface(ctx.Body["path"])
|
||||||
|
s.Auth = func(req *http.Request) string {
|
||||||
|
c, _ := req.Cookie(COOKIE_NAME_AUTH)
|
||||||
|
if c == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var data map[string]string
|
||||||
|
str, err := DecryptString(ctx.Config.General.SecretKey, c.Value)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal([]byte(str), &data); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
boolToString := func(b bool) string {
|
||||||
|
if b == true {
|
||||||
|
return "yes"
|
||||||
|
}
|
||||||
|
return "no"
|
||||||
|
}
|
||||||
|
data["path"] = func(p1 string, p2 string) string{
|
||||||
|
if p1 == "" {
|
||||||
|
return p2
|
||||||
|
}
|
||||||
|
return p1 + strings.TrimPrefix(p2, "/")
|
||||||
|
}(ctx.Session["path"], s.Path)
|
||||||
|
data["can_share"] = boolToString(s.CanShare)
|
||||||
|
data["can_read"] = boolToString(s.CanRead)
|
||||||
|
data["can_write"] = boolToString(s.CanWrite)
|
||||||
|
data["can_upload"] = boolToString(s.CanUpload)
|
||||||
|
|
||||||
|
s, err := json.Marshal(data);
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
obfuscate, err := EncryptString(ctx.Config.General.SecretKey, string(s))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return obfuscate
|
||||||
|
}(req)
|
||||||
|
|
||||||
if err := model.ShareUpsert(&s); err != nil {
|
if err := model.ShareUpsert(&s); err != nil {
|
||||||
SendErrorResult(res, err)
|
SendErrorResult(res, err)
|
||||||
@ -37,13 +92,91 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
SendSuccessResult(res, nil)
|
SendSuccessResult(res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShareGiveProof(ctx App, res http.ResponseWriter, req *http.Request) {
|
func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
// switch NewStringFromInterface(ctx.Body["type"]) {
|
var submittedProof model.Proof
|
||||||
// case "password":
|
var verifiedProof []model.Proof
|
||||||
// case "code": nil
|
var requiredProof []model.Proof
|
||||||
// case "email": nil
|
var remainingProof []model.Proof
|
||||||
// }
|
var s model.Share
|
||||||
SendSuccessResult(res, false)
|
|
||||||
|
// 1) initialise the current context
|
||||||
|
s = extractParams(req, &ctx)
|
||||||
|
if err := model.ShareGet(&s); err != nil {
|
||||||
|
SendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submittedProof = model.Proof{
|
||||||
|
Key: fmt.Sprint(ctx.Body["type"]),
|
||||||
|
Value: fmt.Sprint(ctx.Body["value"]),
|
||||||
|
}
|
||||||
|
verifiedProof = model.ShareProofGetAlreadyVerified(req, &ctx)
|
||||||
|
requiredProof = model.ShareProofGetRequired(s)
|
||||||
|
|
||||||
|
// 2) validate the current context
|
||||||
|
if len(verifiedProof) > 20 || len(requiredProof) > 20 {
|
||||||
|
SendErrorResult(res, NewError("Input error", 405))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := s.IsValid(); err != nil {
|
||||||
|
SendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) process the proof sent by the user
|
||||||
|
submittedProof, err := model.ShareProofVerifier(&ctx, s, submittedProof);
|
||||||
|
if err != nil {
|
||||||
|
submittedProof.Error = NewString(err.Error())
|
||||||
|
SendSuccessResult(res, submittedProof)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if submittedProof.Key == "code" {
|
||||||
|
submittedProof.Value = ""
|
||||||
|
submittedProof.Message = NewString("We've sent you a message with a verification code")
|
||||||
|
SendSuccessResult(res, submittedProof)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if submittedProof.Key != "<nil>" {
|
||||||
|
submittedProof.Id = Hash(submittedProof.Key + "::" + submittedProof.Value)
|
||||||
|
verifiedProof = append(verifiedProof, submittedProof)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Find remaining proofs: requiredProof - verifiedProof
|
||||||
|
remainingProof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
|
||||||
|
|
||||||
|
// log.Println("============")
|
||||||
|
// log.Println("REQUIRED: ", requiredProof)
|
||||||
|
// log.Println("SUBMITTED: ", submittedProof)
|
||||||
|
// log.Println("VERIFIED: ", verifiedProof)
|
||||||
|
// log.Println("REMAINING: ", remainingProof)
|
||||||
|
// log.Println("============")
|
||||||
|
|
||||||
|
// 5) persist proofs in client cookie
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: COOKIE_NAME_PROOF,
|
||||||
|
Value: func(p []model.Proof) string {
|
||||||
|
j, _ := json.Marshal(p)
|
||||||
|
str, _ := EncryptString(ctx.Config.General.SecretKey, string(j))
|
||||||
|
return str
|
||||||
|
}(verifiedProof),
|
||||||
|
Path: COOKIE_PATH,
|
||||||
|
MaxAge: 60 * 60 * 24 * 30,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
http.SetCookie(res, &cookie)
|
||||||
|
|
||||||
|
if len(remainingProof) > 0 {
|
||||||
|
SendSuccessResult(res, remainingProof[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SendSuccessResult(res, struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
Id: s.Id,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) {
|
func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
@ -58,17 +191,18 @@ func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
func extractParams(req *http.Request, ctx *App) model.Share {
|
func extractParams(req *http.Request, ctx *App) model.Share {
|
||||||
return model.Share{
|
return model.Share{
|
||||||
Id: NewStringFromInterface(mux.Vars(req)["id"]),
|
Auth: "",
|
||||||
Backend: NewStringFromInterface(GenerateID(ctx.Session)),
|
Id: NewStringFromInterface(mux.Vars(req)["id"]),
|
||||||
Path: NewStringFromInterface(req.URL.Query().Get("path")),
|
Backend: NewStringFromInterface(GenerateID(ctx.Session)),
|
||||||
Password: NewStringpFromInterface(ctx.Body["password"]),
|
Path: NewStringFromInterface(req.URL.Query().Get("path")),
|
||||||
Users: NewStringpFromInterface(ctx.Body["users"]),
|
Password: NewStringpFromInterface(ctx.Body["password"]),
|
||||||
Expire: NewIntpFromInterface(ctx.Body["expire"]),
|
Users: NewStringpFromInterface(ctx.Body["users"]),
|
||||||
Url: NewStringpFromInterface(ctx.Body["url"]),
|
Expire: NewInt64pFromInterface(ctx.Body["expire"]),
|
||||||
|
Url: NewStringpFromInterface(ctx.Body["url"]),
|
||||||
CanManageOwn: NewBoolFromInterface(ctx.Body["can_manage_own"]),
|
CanManageOwn: NewBoolFromInterface(ctx.Body["can_manage_own"]),
|
||||||
CanShare: NewBoolFromInterface(ctx.Body["can_share"]),
|
CanShare: NewBoolFromInterface(ctx.Body["can_share"]),
|
||||||
CanRead: NewBoolFromInterface(ctx.Body["can_read"]),
|
CanRead: NewBoolFromInterface(ctx.Body["can_read"]),
|
||||||
CanWrite: NewBoolFromInterface(ctx.Body["can_write"]),
|
CanWrite: NewBoolFromInterface(ctx.Body["can_write"]),
|
||||||
CanUpload: NewBoolFromInterface(ctx.Body["can_upload"]),
|
CanUpload: NewBoolFromInterface(ctx.Body["can_upload"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DB *sql.DB
|
var DB *sql.DB
|
||||||
@ -16,19 +17,33 @@ func init() {
|
|||||||
cachePath := filepath.Join(GetCurrentDir(), DBCachePath)
|
cachePath := filepath.Join(GetCurrentDir(), DBCachePath)
|
||||||
os.MkdirAll(cachePath, os.ModePerm)
|
os.MkdirAll(cachePath, os.ModePerm)
|
||||||
var err error
|
var err error
|
||||||
DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true")
|
if DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true"); err != nil {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Location(backend VARCHAR(16), path VARCHAR(512), CONSTRAINT pk_location PRIMARY KEY(backend, path))")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stmt.Exec()
|
|
||||||
|
|
||||||
stmt, err = DB.Prepare("CREATE TABLE IF NOT EXISTS Share(id VARCHAR(64) PRIMARY KEY, related_backend VARCHAR(16), related_path VARCHAR(512), params JSON, FOREIGN KEY (related_backend, related_path) REFERENCES Location(backend, path) ON UPDATE CASCADE ON DELETE CASCADE)")
|
if stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Location(backend VARCHAR(16), path VARCHAR(512), CONSTRAINT pk_location PRIMARY KEY(backend, path))"); err == nil {
|
||||||
if err != nil {
|
stmt.Exec()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
stmt.Exec()
|
|
||||||
|
if stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Share(id VARCHAR(64) PRIMARY KEY, related_backend VARCHAR(16), related_path VARCHAR(512), params JSON, auth VARCHAR(4093) NOT NULL, FOREIGN KEY (related_backend, related_path) REFERENCES Location(backend, path) ON UPDATE CASCADE ON DELETE CASCADE)"); err == nil {
|
||||||
|
stmt.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
if stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Verification(key VARCHAR(512), code VARCHAR(4), expire DATETIME DEFAULT (datetime('now', '+10 minutes')))"); err == nil {
|
||||||
|
stmt.Exec()
|
||||||
|
if stmt, err = DB.Prepare("CREATE INDEX idx_verification ON Verification(code, expire)"); err == nil {
|
||||||
|
stmt.Exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(){
|
||||||
|
autovacuum()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func autovacuum() {
|
||||||
|
if stmt, err := DB.Prepare("DELETE FROM Verification WHERE expire < datetime('now')"); err == nil {
|
||||||
|
stmt.Exec()
|
||||||
|
}
|
||||||
|
time.Sleep(6 * time.Hour)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,38 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
func CanRemoveShare() bool {
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func CanRead(ctx *App) bool {
|
||||||
|
keyword := ctx.Session["can_read"]
|
||||||
|
if keyword == "" || keyword == "yes" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func CanEditShare() bool {
|
func CanEdit(ctx *App) bool {
|
||||||
|
keyword := ctx.Session["can_write"]
|
||||||
|
if keyword == "" || keyword == "yes" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CanUpload(ctx *App) bool {
|
||||||
|
keyword := ctx.Session["can_upload"]
|
||||||
|
if keyword == "" || keyword == "yes" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CanShare(ctx *App) bool {
|
||||||
|
keyword := ctx.Session["can_share"]
|
||||||
|
if keyword == "" || keyword == "yes" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,21 +2,38 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/smtp"
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PASSWORD_DUMMY = "{{PASSWORD}}"
|
const PASSWORD_DUMMY = "{{PASSWORD}}"
|
||||||
|
|
||||||
|
type Proof struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"-"`
|
||||||
|
Message *string `json:"message,omitempty"`
|
||||||
|
Error *string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Share struct {
|
type Share struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Backend string `json:"-"`
|
Backend string `json:"-"`
|
||||||
|
Auth string `json:"auth,omitempty"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Password *string `json:"password,omitempty"`
|
Password *string `json:"password,omitempty"`
|
||||||
Users *string `json:"users,omitempty"`
|
Users *string `json:"users,omitempty"`
|
||||||
Expire *int `json:"expire,omitempty"`
|
Expire *int64 `json:"expire,omitempty"`
|
||||||
Url *string `json:"url,omitempty"`
|
Url *string `json:"url,omitempty"`
|
||||||
CanShare bool `json:"can_share"`
|
CanShare bool `json:"can_share"`
|
||||||
CanManageOwn bool `json:"can_manage_own"`
|
CanManageOwn bool `json:"can_manage_own"`
|
||||||
@ -25,10 +42,27 @@ type Share struct {
|
|||||||
CanUpload bool `json:"can_upload"`
|
CanUpload bool `json:"can_upload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewShare(id string) Share {
|
||||||
|
return Share{
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Share) IsValid() (bool, error) {
|
||||||
|
if s.Expire != nil {
|
||||||
|
now := time.Now().UnixNano() / 1000000
|
||||||
|
if now > *s.Expire {
|
||||||
|
return false, NewError("Link has expired", 410)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Share) MarshalJSON() ([]byte, error) {
|
func (s *Share) MarshalJSON() ([]byte, error) {
|
||||||
p := Share{
|
p := Share{
|
||||||
s.Id,
|
s.Id,
|
||||||
s.Backend,
|
s.Backend,
|
||||||
|
"",
|
||||||
s.Path,
|
s.Path,
|
||||||
func(pass *string) *string{
|
func(pass *string) *string{
|
||||||
if pass != nil {
|
if pass != nil {
|
||||||
@ -57,7 +91,7 @@ func(s *Share) UnmarshallJSON(b []byte) error {
|
|||||||
switch key {
|
switch key {
|
||||||
case "password": s.Password = NewStringpFromInterface(value)
|
case "password": s.Password = NewStringpFromInterface(value)
|
||||||
case "users": s.Users = NewStringpFromInterface(value)
|
case "users": s.Users = NewStringpFromInterface(value)
|
||||||
case "expire": s.Expire = NewIntpFromInterface(value)
|
case "expire": s.Expire = NewInt64pFromInterface(value)
|
||||||
case "url": s.Url = NewStringpFromInterface(value)
|
case "url": s.Url = NewStringpFromInterface(value)
|
||||||
case "can_share": s.CanShare = NewBoolFromInterface(value)
|
case "can_share": s.CanShare = NewBoolFromInterface(value)
|
||||||
case "can_manage_own": s.CanManageOwn = NewBoolFromInterface(value)
|
case "can_manage_own": s.CanManageOwn = NewBoolFromInterface(value)
|
||||||
@ -70,11 +104,11 @@ func(s *Share) UnmarshallJSON(b []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ShareList(p *Share) ([]Share, error) {
|
func ShareList(p *Share) ([]Share, error) {
|
||||||
stmt, err := DB.Prepare("SELECT id, related_path, params FROM Share WHERE related_backend = ?")
|
stmt, err := DB.Prepare("SELECT id, related_path, params FROM Share WHERE related_backend = ? AND related_path LIKE ? || '%' ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rows, err := stmt.Query(p.Backend)
|
rows, err := stmt.Query(p.Backend, p.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -83,7 +117,7 @@ func ShareList(p *Share) ([]Share, error) {
|
|||||||
var a Share
|
var a Share
|
||||||
var params []byte
|
var params []byte
|
||||||
rows.Scan(&a.Id, &a.Path, ¶ms)
|
rows.Scan(&a.Id, &a.Path, ¶ms)
|
||||||
json.Unmarshal(params, &a)
|
json.Unmarshal(params, &a)
|
||||||
sharedFiles = append(sharedFiles, a)
|
sharedFiles = append(sharedFiles, a)
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
@ -91,26 +125,16 @@ func ShareList(p *Share) ([]Share, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ShareGet(p *Share) error {
|
func ShareGet(p *Share) error {
|
||||||
if err := shareGet(p); err != nil {
|
stmt, err := DB.Prepare("SELECT id, related_path, params, auth FROM share WHERE id = ?")
|
||||||
return err
|
|
||||||
}
|
|
||||||
if p.Password != nil {
|
|
||||||
p.Password = NewString(PASSWORD_DUMMY)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shareGet(p *Share) error {
|
|
||||||
stmt, err := DB.Prepare("SELECT id, related_path, params FROM share WHERE id = ?")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer stmt.Close()
|
defer stmt.Close()
|
||||||
row := stmt.QueryRow(p.Id)
|
row := stmt.QueryRow(p.Id)
|
||||||
var str []byte
|
var str []byte
|
||||||
if err = row.Scan(&p.Id, &p.Path, &str); err != nil {
|
if err = row.Scan(&p.Id, &p.Path, &str, &p.Auth); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return NewError("No Result", 404)
|
return NewError("Not Found", 404)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -123,7 +147,7 @@ func ShareUpsert(p *Share) error {
|
|||||||
if *p.Password == PASSWORD_DUMMY {
|
if *p.Password == PASSWORD_DUMMY {
|
||||||
var copy Share
|
var copy Share
|
||||||
copy.Id = p.Id
|
copy.Id = p.Id
|
||||||
shareGet(©);
|
ShareGet(©);
|
||||||
p.Password = copy.Password
|
p.Password = copy.Password
|
||||||
} else {
|
} else {
|
||||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(*p.Password), bcrypt.DefaultCost)
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(*p.Password), bcrypt.DefaultCost)
|
||||||
@ -146,14 +170,14 @@ func ShareUpsert(p *Share) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt, err = DB.Prepare("INSERT INTO Share(id, related_backend, related_path, params) VALUES($1, $2, $3, $4) ON CONFLICT(id) DO UPDATE SET related_backend = $2, related_path = $3, params = $4")
|
stmt, err = DB.Prepare("INSERT INTO Share(id, related_backend, related_path, params, auth) VALUES($1, $2, $3, $4, $5) ON CONFLICT(id) DO UPDATE SET related_backend = $2, related_path = $3, params = $4")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
j, _ := json.Marshal(&struct {
|
j, _ := json.Marshal(&struct {
|
||||||
Password *string `json:"password,omitempty"`
|
Password *string `json:"password,omitempty"`
|
||||||
Users *string `json:"users,omitempty"`
|
Users *string `json:"users,omitempty"`
|
||||||
Expire *int `json:"expire,omitempty"`
|
Expire *int64 `json:"expire,omitempty"`
|
||||||
Url *string `json:"url,omitempty"`
|
Url *string `json:"url,omitempty"`
|
||||||
CanShare bool `json:"can_share"`
|
CanShare bool `json:"can_share"`
|
||||||
CanManageOwn bool `json:"can_manage_own"`
|
CanManageOwn bool `json:"can_manage_own"`
|
||||||
@ -171,7 +195,7 @@ func ShareUpsert(p *Share) error {
|
|||||||
CanWrite: p.CanWrite,
|
CanWrite: p.CanWrite,
|
||||||
CanUpload: p.CanUpload,
|
CanUpload: p.CanUpload,
|
||||||
})
|
})
|
||||||
_, err = stmt.Exec(p.Id, p.Backend, p.Path, j)
|
_, err = stmt.Exec(p.Id, p.Backend, p.Path, j, p.Auth)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,3 +207,461 @@ func ShareDelete(p *Share) error {
|
|||||||
_, err = stmt.Exec(p.Id, p.Backend)
|
_, err = stmt.Exec(p.Id, p.Backend)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
|
||||||
|
p := proof
|
||||||
|
|
||||||
|
if proof.Key == "password" {
|
||||||
|
if s.Password == nil {
|
||||||
|
return p, NewError("No password required", 400)
|
||||||
|
}
|
||||||
|
time.Sleep(1000 * time.Millisecond)
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(*s.Password), []byte(proof.Value)); err != nil {
|
||||||
|
return p, NewError("Invalid Password", 403)
|
||||||
|
}
|
||||||
|
p.Value = *s.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
if proof.Key == "email" {
|
||||||
|
// find out if user is authorized
|
||||||
|
if s.Users == nil {
|
||||||
|
return p, NewError("Authentication not required", 400)
|
||||||
|
}
|
||||||
|
var user *string
|
||||||
|
for _, possibleUser := range strings.Split(*s.Users, ",") {
|
||||||
|
if proof.Value == strings.Trim(possibleUser, " ") {
|
||||||
|
user = &proof.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
time.Sleep(1000 * time.Millisecond)
|
||||||
|
return p, NewError("No access was provided", 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the verification code
|
||||||
|
stmt, err := DB.Prepare("INSERT INTO Verification(key, code) VALUES(?, ?)");
|
||||||
|
if err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
code := RandomString(4)
|
||||||
|
if _, err := stmt.Exec("email::" + proof.Value, code); err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare message
|
||||||
|
var b bytes.Buffer
|
||||||
|
t := template.New("email")
|
||||||
|
t.Parse(TmplEmailVerification())
|
||||||
|
t.Execute(&b, struct{
|
||||||
|
Code string
|
||||||
|
}{code})
|
||||||
|
|
||||||
|
p.Key = "code"
|
||||||
|
p.Value = ""
|
||||||
|
p.Message = NewString("We've sent you a message with a verification code")
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
addr := fmt.Sprintf("%s:%d", ctx.Config.Email.Server, ctx.Config.Email.Port)
|
||||||
|
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
|
||||||
|
subject := "Subject: Your verification code\n"
|
||||||
|
msg := []byte(subject + mime + "\n" + b.String())
|
||||||
|
auth := smtp.PlainAuth("", ctx.Config.Email.Username, ctx.Config.Email.Password, ctx.Config.Email.Server)
|
||||||
|
if err := smtp.SendMail(addr, auth, ctx.Config.Email.From, []string{"mickael@kerjean.me"}, msg); err != nil {
|
||||||
|
log.Println("ERROR: ", err)
|
||||||
|
log.Println("Verification code: " + code)
|
||||||
|
return p, NewError("Couldn't send email", 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if proof.Key == "code" {
|
||||||
|
// find key for given code
|
||||||
|
stmt, err := DB.Prepare("SELECT key FROM Verification WHERE code = ? AND expire > datetime('now')")
|
||||||
|
if err != nil {
|
||||||
|
return p, NewError("Not found", 404)
|
||||||
|
}
|
||||||
|
row := stmt.QueryRow(proof.Value)
|
||||||
|
var key string
|
||||||
|
if err = row.Scan(&key); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
stmt.Close()
|
||||||
|
p.Key = "email"
|
||||||
|
p.Value = ""
|
||||||
|
return p, NewError("Not found", 404)
|
||||||
|
}
|
||||||
|
stmt.Close()
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
stmt.Close()
|
||||||
|
|
||||||
|
// cleanup current attempt so that it isn't used for malicious purpose
|
||||||
|
if stmt, err = DB.Prepare("DELETE FROM Verification WHERE code = ?"); err == nil {
|
||||||
|
stmt.Exec(proof.Value)
|
||||||
|
stmt.Close()
|
||||||
|
}
|
||||||
|
p.Key = "email"
|
||||||
|
p.Value = strings.TrimPrefix(key, "email::")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShareProofGetAlreadyVerified(req *http.Request, ctx *App) []Proof {
|
||||||
|
var p []Proof
|
||||||
|
var cookieValue string
|
||||||
|
|
||||||
|
c, _ := req.Cookie(COOKIE_NAME_PROOF)
|
||||||
|
if c == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
cookieValue = c.Value
|
||||||
|
if len(cookieValue) > 500 {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
j, err := DecryptString(ctx.Config.General.SecretKey, cookieValue)
|
||||||
|
if err != nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal([]byte(j), &p)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShareProofGetRequired(s Share) []Proof {
|
||||||
|
var p []Proof
|
||||||
|
if s.Password != nil {
|
||||||
|
p = append(p, Proof{Key: "password", Value: *s.Password})
|
||||||
|
}
|
||||||
|
if s.Users != nil {
|
||||||
|
p = append(p, Proof{Key: "email", Value: *s.Users})
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShareProofCalculateRemainings(ref []Proof, mem []Proof) []Proof {
|
||||||
|
var remainingProof []Proof
|
||||||
|
|
||||||
|
for i := 0; i < len(ref); i++ {
|
||||||
|
keep := true
|
||||||
|
for j := 0; j < len(mem); j++ {
|
||||||
|
if shareProofAreEquivalent(ref[i], mem[j]) {
|
||||||
|
keep = false
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keep {
|
||||||
|
remainingProof = append(remainingProof, ref[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return remainingProof
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func shareProofAreEquivalent(ref Proof, p Proof) bool {
|
||||||
|
if ref.Key != p.Key {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, chunk := range strings.Split(ref.Value, ",") {
|
||||||
|
chunk = strings.Trim(chunk, " ")
|
||||||
|
if p.Id == Hash(ref.Key + "::" + chunk) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TmplEmailVerification() string {
|
||||||
|
return `
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Nuage code</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 450px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 450px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 490px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">Your code to login</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h2 style="font-weight:100;margin:0">Your verification code is: <strong>{{.Code}}</strong></h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by">
|
||||||
|
Powered by <a href="http://github.com/mickael-kerjean/nuage">Nuage</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
package ctrl
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"github.com/mickael-kerjean/nuage/server/model"
|
|
||||||
. "github.com/mickael-kerjean/nuage/server/common"
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var shareObj = model.Share{
|
var shareObj = Share{
|
||||||
Backend: "foo",
|
Backend: "foo",
|
||||||
Id: "foo",
|
Id: "foo",
|
||||||
Path: "/var/www/",
|
Path: "/var/www/",
|
||||||
@ -16,7 +15,7 @@ var shareObj = model.Share{
|
|||||||
CanRead: true,
|
CanRead: true,
|
||||||
CanManageOwn: true,
|
CanManageOwn: true,
|
||||||
CanShare: true,
|
CanShare: true,
|
||||||
Expire: NewInt(1537759505787),
|
Expire: NewInt64(1537759505787),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -24,28 +23,28 @@ var shareObj = model.Share{
|
|||||||
//// UPSERT
|
//// UPSERT
|
||||||
|
|
||||||
func TestShareSimpleUpsert(t *testing.T) {
|
func TestShareSimpleUpsert(t *testing.T) {
|
||||||
err := model.ShareUpsert(&shareObj);
|
err := ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShareMultipleUpsert(t *testing.T) {
|
func TestShareMultipleUpsert(t *testing.T) {
|
||||||
err := model.ShareUpsert(&shareObj);
|
err := ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = model.ShareUpsert(&shareObj);
|
err = ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = model.ShareGet(&shareObj)
|
err = ShareGet(&shareObj)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShareUpsertIsProperlyInserted(t *testing.T) {
|
func TestShareUpsertIsProperlyInserted(t *testing.T) {
|
||||||
err := model.ShareUpsert(&shareObj);
|
err := ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var obj model.Share
|
var obj Share
|
||||||
obj.Id = "foo"
|
obj.Id = "foo"
|
||||||
err = model.ShareGet(&obj)
|
err = ShareGet(&obj)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, obj.Password)
|
assert.NotNil(t, obj.Password)
|
||||||
}
|
}
|
||||||
@ -54,28 +53,28 @@ func TestShareUpsertIsProperlyInserted(t *testing.T) {
|
|||||||
//// get
|
//// get
|
||||||
|
|
||||||
func TestShareGetNonExisting(t *testing.T) {
|
func TestShareGetNonExisting(t *testing.T) {
|
||||||
var s model.Share = shareObj
|
var s Share = shareObj
|
||||||
s.Id = "nothing"
|
s.Id = "nothing"
|
||||||
err := model.ShareGet(&s);
|
err := ShareGet(&s);
|
||||||
assert.Error(t, err, "Shouldn't be able to get something that doesn't exist yet")
|
assert.Error(t, err, "Shouldn't be able to get something that doesn't exist yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShareGetExisting(t *testing.T) {
|
func TestShareGetExisting(t *testing.T) {
|
||||||
err := model.ShareUpsert(&shareObj);
|
err := ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err, "Upsert issue")
|
assert.NoError(t, err, "Upsert issue")
|
||||||
|
|
||||||
err = model.ShareGet(&shareObj);
|
err = ShareGet(&shareObj);
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
|
func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
|
||||||
err := model.ShareUpsert(&shareObj);
|
err := ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err, "Upsert issue")
|
assert.NoError(t, err, "Upsert issue")
|
||||||
|
|
||||||
var obj model.Share
|
var obj Share
|
||||||
obj.Id = "foo"
|
obj.Id = "foo"
|
||||||
obj.Backend = shareObj.Backend
|
obj.Backend = shareObj.Backend
|
||||||
err = model.ShareGet(&obj);
|
err = ShareGet(&obj);
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "foo", obj.Id)
|
assert.Equal(t, "foo", obj.Id)
|
||||||
assert.Equal(t, "/var/www/", obj.Path)
|
assert.Equal(t, "/var/www/", obj.Path)
|
||||||
@ -85,8 +84,7 @@ func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
|
|||||||
assert.Equal(t, false, obj.CanWrite)
|
assert.Equal(t, false, obj.CanWrite)
|
||||||
assert.Equal(t, false, obj.CanUpload)
|
assert.Equal(t, false, obj.CanUpload)
|
||||||
assert.Equal(t, "foo", obj.Backend)
|
assert.Equal(t, "foo", obj.Backend)
|
||||||
assert.Equal(t, shareObj.Expire, obj.Expire)
|
assert.Equal(t, shareObj.Expire, obj.Expire)
|
||||||
assert.Equal(t, "{{PASSWORD}}", *obj.Password)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
@ -94,11 +92,11 @@ func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
|
|||||||
|
|
||||||
func TestShareListAll(t *testing.T) {
|
func TestShareListAll(t *testing.T) {
|
||||||
// Initialise test
|
// Initialise test
|
||||||
err := model.ShareUpsert(&shareObj);
|
err := ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err, "Upsert issue")
|
assert.NoError(t, err, "Upsert issue")
|
||||||
|
|
||||||
// Actual test
|
// Actual test
|
||||||
list, err := model.ShareList(&shareObj)
|
list, err := ShareList(&shareObj)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, list, 1)
|
assert.Len(t, list, 1)
|
||||||
assert.NotNil(t, list[0].Password)
|
assert.NotNil(t, list[0].Password)
|
||||||
@ -110,15 +108,49 @@ func TestShareListAll(t *testing.T) {
|
|||||||
|
|
||||||
func TestShareDeleteShares(t *testing.T) {
|
func TestShareDeleteShares(t *testing.T) {
|
||||||
// Initialise test
|
// Initialise test
|
||||||
err := model.ShareUpsert(&shareObj);
|
err := ShareUpsert(&shareObj);
|
||||||
assert.NoError(t, err, "Upsert issue")
|
assert.NoError(t, err, "Upsert issue")
|
||||||
err = model.ShareGet(&shareObj)
|
err = ShareGet(&shareObj)
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Actual Test
|
|
||||||
err = model.ShareDelete(&shareObj);
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = model.ShareGet(&shareObj)
|
// Actual Test
|
||||||
assert.Error(t, err)
|
err = ShareDelete(&shareObj);
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = ShareGet(&shareObj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
//// PROOF
|
||||||
|
|
||||||
|
func TestShareVerifyEquivalence(t *testing.T) {
|
||||||
|
p1 := Proof {
|
||||||
|
Key: "password",
|
||||||
|
Value: "I'm something random",
|
||||||
|
}
|
||||||
|
p2 := Proof {
|
||||||
|
Key: p1.Key,
|
||||||
|
Id: "hash",
|
||||||
|
}
|
||||||
|
res := ShareProofAreEquivalent(p1, p2)
|
||||||
|
assert.Equal(t, false, res)
|
||||||
|
|
||||||
|
p2.Id = Hash(p1.Key + "::" + p1.Value)
|
||||||
|
res = ShareProofAreEquivalent(p1, p2)
|
||||||
|
assert.Equal(t, true, res)
|
||||||
|
|
||||||
|
p2.Key = "email"
|
||||||
|
res = ShareProofAreEquivalent(p1, p2)
|
||||||
|
assert.Equal(t, false, res)
|
||||||
|
|
||||||
|
p1.Key = "email"
|
||||||
|
p1.Value = "test@gmail.com,polo@gmail.com,jean@gmail.com"
|
||||||
|
p2.Key = "email"
|
||||||
|
p2.Id = Hash(p1.Key + "::" + "polo@gmail.com")
|
||||||
|
res = ShareProofAreEquivalent(p1, p2)
|
||||||
|
assert.Equal(t, true, res)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -33,8 +33,8 @@ func Init(a *App) *http.Server {
|
|||||||
share.HandleFunc("/{id}", APIHandler(ShareGet, *a)).Methods("GET")
|
share.HandleFunc("/{id}", APIHandler(ShareGet, *a)).Methods("GET")
|
||||||
share.HandleFunc("/{id}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
share.HandleFunc("/{id}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
||||||
share.HandleFunc("/{id}", APIHandler(ShareDelete, *a)).Methods("DELETE")
|
share.HandleFunc("/{id}", APIHandler(ShareDelete, *a)).Methods("DELETE")
|
||||||
r.HandleFunc("/api/proof", APIHandler(ShareGiveProof, *a)).Methods("GET")
|
share.HandleFunc("/{id}/proof", APIHandler(ShareVerifyProof, *a)).Methods("POST")
|
||||||
|
|
||||||
// APP
|
// APP
|
||||||
r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET")
|
r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET")
|
||||||
r.PathPrefix("/assets").Handler(StaticHandler("./data/public/", *a)).Methods("GET")
|
r.PathPrefix("/assets").Handler(StaticHandler("./data/public/", *a)).Methods("GET")
|
||||||
|
|||||||
@ -17,6 +17,7 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
|
|||||||
start := time.Now()
|
start := time.Now()
|
||||||
ctx.Body, _ = extractBody(req)
|
ctx.Body, _ = extractBody(req)
|
||||||
ctx.Session, _ = extractSession(req, &ctx)
|
ctx.Session, _ = extractSession(req, &ctx)
|
||||||
|
|
||||||
ctx.Backend, _ = extractBackend(req, &ctx)
|
ctx.Backend, _ = extractBackend(req, &ctx)
|
||||||
res.Header().Add("Content-Type", "application/json")
|
res.Header().Add("Content-Type", "application/json")
|
||||||
|
|
||||||
@ -65,11 +66,36 @@ func extractBody(req *http.Request) (map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractSession(req *http.Request, ctx *App) (map[string]string, error) {
|
func extractSession(req *http.Request, ctx *App) (map[string]string, error) {
|
||||||
cookie, err := req.Cookie(COOKIE_NAME)
|
var str string
|
||||||
if err != nil {
|
var res map[string]string
|
||||||
return make(map[string]string), err
|
|
||||||
|
if req.URL.Query().Get("share") != "" {
|
||||||
|
s := model.NewShare(req.URL.Query().Get("share"))
|
||||||
|
if err := model.ShareGet(&s); err != nil {
|
||||||
|
return make(map[string]string), err
|
||||||
|
}
|
||||||
|
if _, err := s.IsValid(); err != nil {
|
||||||
|
return make(map[string]string), err
|
||||||
|
}
|
||||||
|
|
||||||
|
var verifiedProof []model.Proof = model.ShareProofGetAlreadyVerified(req, ctx)
|
||||||
|
var requiredProof []model.Proof = model.ShareProofGetRequired(s)
|
||||||
|
var remainingProof []model.Proof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
|
||||||
|
if len(remainingProof) != 0 {
|
||||||
|
return make(map[string]string), NewError("Unauthorized Shared space", 400)
|
||||||
|
}
|
||||||
|
str = s.Auth
|
||||||
|
} else {
|
||||||
|
cookie, err := req.Cookie(COOKIE_NAME_AUTH)
|
||||||
|
if err != nil {
|
||||||
|
return make(map[string]string), err
|
||||||
|
}
|
||||||
|
str = cookie.Value
|
||||||
}
|
}
|
||||||
return Decrypt(ctx.Config.General.SecretKey, cookie.Value)
|
|
||||||
|
str, err := DecryptString(ctx.Config.General.SecretKey, str)
|
||||||
|
err = json.Unmarshal([]byte(str), &res)
|
||||||
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractBackend(req *http.Request, ctx *App) (IBackend, error) {
|
func extractBackend(req *http.Request, ctx *App) (IBackend, error) {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ func ProcessFileBeforeSend(reader io.Reader, ctx *App, req *http.Request, res *h
|
|||||||
/////////////////////////
|
/////////////////////////
|
||||||
// Specify transformation
|
// Specify transformation
|
||||||
transform := &images.Transform{
|
transform := &images.Transform{
|
||||||
Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + RandomString(10)),
|
Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + QuickString(10)),
|
||||||
Size: 300,
|
Size: 300,
|
||||||
Crop: true,
|
Crop: true,
|
||||||
Quality: 50,
|
Quality: 50,
|
||||||
|
|||||||
Reference in New Issue
Block a user