mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-02 20:23:32 +08:00
feature (Share): workable version for sharing
This commit is contained in:
@ -50,11 +50,15 @@
|
||||
--secondary: #466372;
|
||||
--emphasis-secondary: #466372;
|
||||
--light: #909090;
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
--super-light: #f4f4f4;
|
||||
=======
|
||||
--super-light: #F7F9FA;
|
||||
>>>>>>> 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;
|
||||
--success: #63d9b1;
|
||||
--dark: #313538;
|
||||
|
||||
@ -45,7 +45,7 @@ export class BreadCrumb extends React.Component {
|
||||
}
|
||||
|
||||
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;
|
||||
return (
|
||||
@ -136,7 +136,7 @@ export class PathElementWrapper extends React.Component {
|
||||
return (
|
||||
<li className={className}>
|
||||
<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}>
|
||||
{this.limitSize(this.props.path.label)}
|
||||
</NgIf>
|
||||
|
||||
@ -23,4 +23,6 @@ button{
|
||||
background: var(--emphasis);
|
||||
color: white
|
||||
}
|
||||
&.transparent{
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,10 @@ export class Container extends React.Component {
|
||||
}
|
||||
render() {
|
||||
const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {};
|
||||
let className = "component_container";
|
||||
if(this.props.className) className += " "+this.props.className;
|
||||
return (
|
||||
<div className="component_container" style={style}>
|
||||
<div className={className} style={style}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { Session } from '../model/';
|
||||
import { Container, Loader, Icon } from '../components/';
|
||||
import { memory } from '../helpers/';
|
||||
import { memory, currentShare } from '../helpers/';
|
||||
|
||||
import '../pages/error.scss';
|
||||
|
||||
@ -19,7 +19,7 @@ export function LoggedInOnly(WrappedComponent){
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if(this.state.is_logged_in === false){
|
||||
if(this.state.is_logged_in === false && currentShare() === null){
|
||||
Session.currentUser().then((res) => {
|
||||
if(res.is_authenticated === false){
|
||||
this.props.error({message: "Authentication Required"});
|
||||
@ -38,7 +38,7 @@ export function LoggedInOnly(WrappedComponent){
|
||||
}
|
||||
|
||||
render(){
|
||||
if(this.state.is_logged_in === true){
|
||||
if(this.state.is_logged_in === true || currentShare() !== null){
|
||||
return <WrappedComponent {...this.props} />;
|
||||
}
|
||||
return null;
|
||||
@ -65,7 +65,7 @@ export function ErrorPage(WrappedComponent){
|
||||
const message = this.state.error.message || "There is nothing in here";
|
||||
return (
|
||||
<div>
|
||||
<Link to="/" className="backnav">
|
||||
<Link to={`/${window.location.search}`} className="backnav">
|
||||
<Icon name="arrow_left" />home
|
||||
</Link>
|
||||
<Container>
|
||||
|
||||
@ -7,7 +7,7 @@ function Data(){
|
||||
this._init();
|
||||
}
|
||||
|
||||
const DB_VERSION = 2;
|
||||
const DB_VERSION = 3;
|
||||
|
||||
Data.prototype._init = function(){
|
||||
const request = indexedDB.open('nuage', DB_VERSION);
|
||||
@ -32,25 +32,29 @@ Data.prototype._setup = function(e){
|
||||
// to make sure everything will be fine
|
||||
db.deleteObjectStore(this.FILE_PATH);
|
||||
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.createIndex("idx_path", "path", { unique: true });
|
||||
store = db.createObjectStore(this.FILE_PATH, {keyPath: ["share", "path"]});
|
||||
store.createIndex("idx_path", ["share", "path"], { unique: true });
|
||||
|
||||
store = db.createObjectStore(this.FILE_CONTENT, {keyPath: "path"});
|
||||
store.createIndex("idx_path", "path", { unique: true });
|
||||
store = db.createObjectStore(this.FILE_CONTENT, {keyPath: ["share", "path"]});
|
||||
store.createIndex("idx_path", ["share", "path"], { unique: true });
|
||||
}
|
||||
|
||||
/*
|
||||
* 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({});
|
||||
|
||||
return this.db.then((db) => {
|
||||
const tx = db.transaction(type, "readonly");
|
||||
const store = tx.objectStore(type);
|
||||
const query = store.get(path);
|
||||
const query = store.get(key);
|
||||
return new Promise((done, error) => {
|
||||
query.onsuccess = (e) => {
|
||||
let data = query.result;
|
||||
@ -61,13 +65,13 @@ Data.prototype.get = function(type, path){
|
||||
}).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) => {
|
||||
const tx = db.transaction(type, "readwrite");
|
||||
const store = tx.objectStore(type);
|
||||
const range = exact === true? IDBKeyRange.only(path) : IDBKeyRange.bound(
|
||||
path,
|
||||
path+'\uFFFF',
|
||||
const range = exact === true? IDBKeyRange.only(key) : IDBKeyRange.bound(
|
||||
[key[0], key[1]],
|
||||
[key[0], key[1]+'\uFFFF'],
|
||||
false, true
|
||||
);
|
||||
const request = store.openCursor(range);
|
||||
@ -77,7 +81,7 @@ Data.prototype.update = function(type, path, fn, exact = true){
|
||||
const cursor = event.target.result;
|
||||
if(!cursor) return done(new_data);
|
||||
new_data = fn(cursor.value || null);
|
||||
cursor.delete(cursor.value.path);
|
||||
cursor.delete([key[0], cursor.value.path]);
|
||||
store.put(new_data);
|
||||
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) => {
|
||||
const tx = db.transaction(type, "readwrite");
|
||||
const store = tx.objectStore(type);
|
||||
const query = store.get(path);
|
||||
const query = store.get(key);
|
||||
return new Promise((done, error) => {
|
||||
query.onsuccess = (e) => {
|
||||
const new_data = fn(query.result || null);
|
||||
@ -105,7 +109,7 @@ Data.prototype.upsert = function(type, path, fn){
|
||||
}).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({});
|
||||
|
||||
return this.db.then((db) => {
|
||||
@ -119,28 +123,28 @@ Data.prototype.add = function(type, path, data){
|
||||
}).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) => {
|
||||
const tx = db.transaction(type, "readwrite");
|
||||
const store = tx.objectStore(type);
|
||||
|
||||
if(exact === true){
|
||||
const req = store.delete(path);
|
||||
const req = store.delete(key);
|
||||
return new Promise((done, err) => {
|
||||
req.onsuccess = () => done();
|
||||
req.onerror = err;
|
||||
});
|
||||
}else{
|
||||
const request = store.openCursor(IDBKeyRange.bound(
|
||||
path,
|
||||
path+'\uFFFF',
|
||||
[key[0], key[1]],
|
||||
[key[0], key[1]+'\uFFFF'],
|
||||
true, true
|
||||
));
|
||||
return new Promise((done, err) => {
|
||||
request.onsuccess = function(event) {
|
||||
const cursor = event.target.result;
|
||||
if(cursor){
|
||||
cursor.delete(cursor.value.path);
|
||||
cursor.delete([key[0], cursor.value.path]);
|
||||
cursor.continue();
|
||||
}else{
|
||||
done();
|
||||
@ -151,12 +155,15 @@ Data.prototype.remove = function(type, path, exact = true){
|
||||
}).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) => {
|
||||
const tx = db.transaction([type], "readonly");
|
||||
const store = tx.objectStore(type);
|
||||
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) => {
|
||||
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;
|
||||
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 { event } from './events';
|
||||
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 { prepare } from './navigate';
|
||||
export { invalidate, http_get, http_post, http_delete } from './ajax';
|
||||
export { prompt, alert, confirm } from './popup';
|
||||
export { notify } from './notify';
|
||||
export { gid, randomString } from './random';
|
||||
export { leftPad } from './common';
|
||||
export { leftPad, copyToClipboard } from './common';
|
||||
export { getMimeType } from './mimetype';
|
||||
export { settings_get, settings_put } from './settings';
|
||||
|
||||
@ -35,3 +35,19 @@ export function absoluteToRelative(from, to){
|
||||
}
|
||||
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";
|
||||
|
||||
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 { cache } from '../helpers/';
|
||||
@ -46,16 +46,17 @@ class FileSystem{
|
||||
}
|
||||
|
||||
_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 cache.upsert(cache.FILE_PATH, path, (_files) => {
|
||||
return cache.upsert(cache.FILE_PATH, [currentShare(), path], (_files) => {
|
||||
let store = Object.assign({
|
||||
share: currentShare(),
|
||||
path: path,
|
||||
results: null,
|
||||
access_count: 0,
|
||||
metadata: null
|
||||
}, _files);
|
||||
store.access_count += 1;
|
||||
store.results = response.results || [];
|
||||
store.results = store.results.map((f) => {
|
||||
f.path = pathBuilder(path, f.name, f.type);
|
||||
@ -64,6 +65,8 @@ class FileSystem{
|
||||
store.metadata = response.metadata;
|
||||
|
||||
if(_files && _files.results){
|
||||
store.access_count = _files.access_count;
|
||||
|
||||
// find out which entry we want to keep from the cache
|
||||
let _files_virtual_to_keep = _files.results.filter((file) => {
|
||||
return file.icon === 'loading';
|
||||
@ -101,8 +104,7 @@ class FileSystem{
|
||||
}
|
||||
|
||||
_ls_from_cache(path, _record_access = false){
|
||||
if(_record_access === false){
|
||||
return cache.get(cache.FILE_PATH, path).then((response) => {
|
||||
return cache.get(cache.FILE_PATH, [currentShare(), path]).then((response) => {
|
||||
if(!response || !response.results) return null;
|
||||
if(this.current_path === path){
|
||||
this.obs && this.obs.next({
|
||||
@ -112,9 +114,10 @@ class FileSystem{
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}else{
|
||||
return cache.upsert(cache.FILE_PATH, path, (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({
|
||||
@ -128,17 +131,20 @@ class FileSystem{
|
||||
return response;
|
||||
});
|
||||
}
|
||||
});
|
||||
return Promise.resolve(e);
|
||||
});
|
||||
}
|
||||
|
||||
rm(path){
|
||||
const url = '/api/files/rm?path='+prepare(path);
|
||||
const url = appendShareToUrl('/api/files/rm?path='+prepare(path));
|
||||
return this._replace(path, 'loading')
|
||||
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
|
||||
.then(() => http_get(url))
|
||||
.then((res) => {
|
||||
return cache.remove(cache.FILE_CONTENT, path)
|
||||
.then(cache.remove(cache.FILE_CONTENT, path, false))
|
||||
.then(cache.remove(cache.FILE_PATH, dirname(path), false))
|
||||
return cache.remove(cache.FILE_CONTENT, [currentShare(), path])
|
||||
.then(cache.remove(cache.FILE_CONTENT, [currentShare(), path], false))
|
||||
.then(cache.remove(cache.FILE_PATH, [currentShare(), dirname(path)], false))
|
||||
.then(this._remove(path, 'loading'))
|
||||
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
|
||||
})
|
||||
@ -150,14 +156,15 @@ class FileSystem{
|
||||
}
|
||||
|
||||
cat(path){
|
||||
const url = '/api/files/cat?path='+prepare(path);
|
||||
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||
return http_get(url, 'raw')
|
||||
.then((res) => {
|
||||
if(this.is_binary(res) === true){
|
||||
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 : {
|
||||
share: currentShare(),
|
||||
path: path,
|
||||
last_update: null,
|
||||
last_access: null,
|
||||
@ -173,7 +180,7 @@ class FileSystem{
|
||||
.catch((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.access_count += 1;
|
||||
return response;
|
||||
@ -184,12 +191,12 @@ class FileSystem{
|
||||
});
|
||||
}
|
||||
url(path){
|
||||
const url = '/api/files/cat?path='+prepare(path);
|
||||
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||
return Promise.resolve(url);
|
||||
}
|
||||
|
||||
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();
|
||||
formData.append('file', file, "test");
|
||||
return this._replace(path, 'loading')
|
||||
@ -207,7 +214,7 @@ class FileSystem{
|
||||
}
|
||||
|
||||
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'),
|
||||
destination_path = path;
|
||||
|
||||
@ -240,8 +247,9 @@ class FileSystem{
|
||||
.then(() => {
|
||||
return this._replace(destination_path, null, 'loading')
|
||||
.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,
|
||||
share: currentShare(),
|
||||
results: [],
|
||||
access_count: 0,
|
||||
last_access: null,
|
||||
@ -310,12 +318,12 @@ class FileSystem{
|
||||
|
||||
function query(){
|
||||
if(file){
|
||||
const url = '/api/files/cat?path='+prepare(path);
|
||||
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||
let formData = new window.FormData();
|
||||
formData.append('file', file);
|
||||
return http_post(url, formData, 'multipart');
|
||||
}else{
|
||||
const url = '/api/files/touch?path='+prepare(path);
|
||||
const url = appendShareToUrl('/api/files/touch?path='+prepare(path));
|
||||
return http_get(url);
|
||||
}
|
||||
}
|
||||
@ -331,7 +339,7 @@ class FileSystem{
|
||||
}
|
||||
|
||||
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,
|
||||
destination_path = to;
|
||||
|
||||
@ -344,11 +352,11 @@ class FileSystem{
|
||||
.then(() => this._replace(destination_path, null, 'loading'))
|
||||
.then(() => this._refresh(origin_path, destination_path))
|
||||
.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);
|
||||
return data;
|
||||
}, 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);
|
||||
return data;
|
||||
}, false);
|
||||
@ -369,7 +377,7 @@ class FileSystem{
|
||||
if(value.access_count >= 1 && value.path !== "/"){
|
||||
data.push(value);
|
||||
}
|
||||
}).then(() => {
|
||||
}, cache.FILE_PATH, [currentShare(), "/"]).then(() => {
|
||||
return Promise.resolve(
|
||||
data
|
||||
.sort((a,b) => a.access_count > b.access_count? -1 : 1)
|
||||
@ -389,8 +397,9 @@ class FileSystem{
|
||||
});
|
||||
|
||||
function update_cache(result){
|
||||
return cache.upsert(cache.FILE_CONTENT, path, (response) => {
|
||||
return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => {
|
||||
if(!response) response = {
|
||||
share: currentShare(),
|
||||
path: path,
|
||||
last_access: null,
|
||||
last_update: null,
|
||||
@ -413,7 +422,7 @@ class FileSystem{
|
||||
}
|
||||
|
||||
_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) => {
|
||||
if(file.name === basename(path) && file.icon == icon_previous){
|
||||
if(!icon){ delete file.icon; }
|
||||
@ -425,7 +434,7 @@ class FileSystem{
|
||||
});
|
||||
}
|
||||
_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){
|
||||
res = {
|
||||
path: dirname(path),
|
||||
@ -445,7 +454,7 @@ class FileSystem{
|
||||
});
|
||||
}
|
||||
_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;
|
||||
res.results = res.results.filter((file) => {
|
||||
return file.name === basename(path) && file.icon == previous_icon ? false : true;
|
||||
@ -461,5 +470,4 @@ class 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 {
|
||||
constructor(){}
|
||||
@ -25,19 +25,20 @@ class ShareModel {
|
||||
}
|
||||
|
||||
upsert(obj){
|
||||
const url = `/api/share/${obj.id}`
|
||||
const url = appendShareToUrl(`/api/share/${obj.id}`)
|
||||
const data = Object.assign({}, obj);
|
||||
delete data.role;
|
||||
return http_post(url, data);
|
||||
}
|
||||
|
||||
remove(id){
|
||||
const url = `/api/share/${id}`;
|
||||
const url = appendShareToUrl(`/api/share/${id}`);
|
||||
return http_delete(url);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;}
|
||||
a{border-bottom: 1px dashed;}
|
||||
}
|
||||
|
||||
.backnav {
|
||||
font-weight: 100;
|
||||
.component_icon {
|
||||
height: 25px;
|
||||
margin-right: -2px;
|
||||
height: 23px;
|
||||
margin-right: -3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Files } from '../model/';
|
||||
import { notify, alert } from '../helpers/';
|
||||
import { notify, alert, currentShare } from '../helpers/';
|
||||
import Path from 'path';
|
||||
import Worker from "../worker/search.worker.js";
|
||||
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 = "/") => {
|
||||
worker.postMessage({
|
||||
action: "search::find",
|
||||
path: path,
|
||||
share: currentShare(),
|
||||
keyword: keyword
|
||||
});
|
||||
return new Observable((obs) => {
|
||||
|
||||
@ -244,6 +244,9 @@ export class FilesPage extends React.Component {
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
</div>
|
||||
<div className="sidebar close">
|
||||
THIS IS A MENUBAR
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -25,6 +25,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar{
|
||||
width: 250px;
|
||||
transition: width 0.3s ease;
|
||||
background: var(--light);
|
||||
&.close{width: 0;}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-y{
|
||||
|
||||
@ -17,11 +17,12 @@ export class FrequentlyAccess extends React.Component {
|
||||
return (
|
||||
<ReactCSSTransitionGroup transitionName="frequent-access" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={300}>
|
||||
<Container>
|
||||
<span>Quick Access</span>
|
||||
<div className="component_frequently-access">
|
||||
{
|
||||
this.props.files.map(function(path, index){
|
||||
return (
|
||||
<Link key={path} to={"/files"+path}>
|
||||
<Link key={path} to={"/files"+path+window.location.search}>
|
||||
<Icon name={'directory'} />
|
||||
<div>{Path.basename(path)}</div>
|
||||
</Link>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
a{
|
||||
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;
|
||||
overflow: hidden;
|
||||
margin-right: 5px;
|
||||
|
||||
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { NgIf, Icon } from '../../components/';
|
||||
import { Share } from '../../model/';
|
||||
import { randomString, notify, absoluteToRelative } from '../../helpers/';
|
||||
import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/';
|
||||
import './share.scss';
|
||||
|
||||
export class ShareComponent extends React.Component {
|
||||
@ -32,14 +32,20 @@ export class ShareComponent extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
Share.all(this.props.path)
|
||||
.then((existings) => {
|
||||
Share.all(this.props.path).then((existings) => {
|
||||
this.refreshModal();
|
||||
this.setState({existings: existings});
|
||||
this.setState({
|
||||
existings: existings.sort((a, b) => {
|
||||
return a.path.split("/").length > b.path.split("/").length;
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateState(key, value){
|
||||
if(key === "role"){
|
||||
this.setState(this.resetState());
|
||||
}
|
||||
if(this.state[key] === value){
|
||||
this.setState({[key]: null});
|
||||
}else{
|
||||
@ -85,10 +91,13 @@ export class ShareComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onRegisterLink(e){
|
||||
this.refs.$input.select();
|
||||
document.execCommand("copy");
|
||||
copyLinkInClipboard(link){
|
||||
copyToClipboard(link);
|
||||
notify.send("The link was copied in the clipboard", "INFO");
|
||||
}
|
||||
|
||||
onRegisterLink(e){
|
||||
this.copyLinkInClipboard(this.refs.$input.value);
|
||||
|
||||
const link = {
|
||||
role: this.state.role,
|
||||
@ -137,9 +146,9 @@ export class ShareComponent extends React.Component {
|
||||
return Share.upsert(link)
|
||||
.then(() => {
|
||||
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()))
|
||||
.catch((err) => {
|
||||
@ -152,11 +161,13 @@ export class ShareComponent extends React.Component {
|
||||
|
||||
render(){
|
||||
const beautifulPath = function(from, to){
|
||||
return to;
|
||||
const p = absoluteToRelative(from, to);
|
||||
if(p === "./"){
|
||||
return "Current folder";
|
||||
if(filetype(from) === "directory"){
|
||||
from = from.split("/");
|
||||
from = from.slice(0, from.length - 1);
|
||||
from = from.join("/");
|
||||
}
|
||||
|
||||
let p = absoluteToRelative(from, to);
|
||||
return p.length < to.length ? p : to;
|
||||
};
|
||||
const urlify = function(str){
|
||||
@ -197,8 +208,10 @@ export class ShareComponent extends React.Component {
|
||||
this.state.existings && this.state.existings.map((link, i) => {
|
||||
return (
|
||||
<div className="link-details" key={i}>
|
||||
<span className="role">{link.role}</span>
|
||||
<span className="path">{beautifulPath(this.props.path, link.path)}</span>
|
||||
<span onClick={this.copyLinkInClipboard.bind(this, window.location.origin+"/s/"+link.id)} className="copy role">
|
||||
{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.onLoad.bind(this, link)} name="edit"/>
|
||||
</div>
|
||||
@ -221,16 +234,18 @@ export class ShareComponent extends React.Component {
|
||||
<NgIf type="inline" cond={!this.state.show_advanced}><Icon name="arrow_bottom"/></NgIf>
|
||||
</h2>
|
||||
<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_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={this.state.url} label="Custom Link url" placeholder="beautiful_url" onChange={(val) => this.updateState('url', urlify(val))} inputType="text"/>
|
||||
</NgIf>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</NgIf>
|
||||
</div>
|
||||
@ -240,15 +255,18 @@ export class ShareComponent extends React.Component {
|
||||
|
||||
const SuperCheckbox = (props) => {
|
||||
const onCheckboxTick = (e) => {
|
||||
if(props.inputType === undefined){
|
||||
return props.onChange(e.target.checked ? true : false);
|
||||
}
|
||||
return props.onChange(e.target.checked ? "" : null);
|
||||
};
|
||||
const onValueChange = (e) => {
|
||||
props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
const _is_expended = function(val){
|
||||
return val === null || val === undefined ? false : true;
|
||||
return val === null || val === undefined || val === false ? false : true;
|
||||
}(props.value);
|
||||
|
||||
return (
|
||||
<div className="component_supercheckbox">
|
||||
<label>
|
||||
|
||||
@ -9,6 +9,9 @@
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.copy {
|
||||
cursor: copy;
|
||||
}
|
||||
.share--content{
|
||||
margin-bottom: 10px;
|
||||
|
||||
@ -53,7 +56,7 @@
|
||||
.path{
|
||||
vertical-align: middle;
|
||||
font-size: 0.9em;
|
||||
max-width: 180px;
|
||||
max-width: 170px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
|
||||
@ -5,6 +5,8 @@ import { Card, NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList,
|
||||
import { pathBuilder, debounce } from '../../helpers/';
|
||||
import "./submenu.scss";
|
||||
|
||||
let SEARCH_KEYWORD = "";
|
||||
|
||||
@EventEmitter
|
||||
export class Submenu extends React.Component {
|
||||
constructor(props){
|
||||
@ -14,6 +16,12 @@ export class Submenu extends React.Component {
|
||||
search_input_visible: false,
|
||||
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._onEscapeKeyPress = (e) => {
|
||||
if(e.keyCode === 27){ // escape key
|
||||
@ -38,8 +46,12 @@ export class Submenu extends React.Component {
|
||||
|
||||
componentDidMount(){
|
||||
window.addEventListener('keydown', this._onEscapeKeyPress);
|
||||
if(this.state.search_input_visible === true){
|
||||
this.onSearchChange(this.state.search_keyword);
|
||||
}
|
||||
}
|
||||
componentWillUnmount(){
|
||||
SEARCH_KEYWORD = this.state.search_keyword;
|
||||
window.removeEventListener('keydown', this._onEscapeKeyPress);
|
||||
}
|
||||
|
||||
|
||||
@ -214,12 +214,12 @@ export class ExistingThing extends React.Component {
|
||||
|
||||
return connectDragSource(connectDropNativeFile(connectDropFile(
|
||||
<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}>
|
||||
<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)}/>
|
||||
<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>
|
||||
</Link>
|
||||
</div>
|
||||
@ -310,7 +310,7 @@ const ActionButton = (props) => {
|
||||
<NgIf cond={props.can_delete !== false} type="inline">
|
||||
<Icon name="delete" onClick={onDelete} className="component_updater--icon"/>
|
||||
</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"/>
|
||||
</NgIf>
|
||||
</div>
|
||||
|
||||
@ -2,40 +2,60 @@ import React from 'react';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
import { Share } from '../model/';
|
||||
import { notify } from '../helpers/';
|
||||
import { Loader, Input, Button, Container } from '../components/';
|
||||
import { notify, basename, filetype } from '../helpers/';
|
||||
import { Loader, Input, Button, Container, ErrorPage, Icon, NgIf } from '../components/';
|
||||
import './error.scss';
|
||||
import './sharepage.scss';
|
||||
|
||||
@ErrorPage
|
||||
export class SharePage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
redirection: null,
|
||||
loading: true,
|
||||
request_password: false,
|
||||
request_username: false
|
||||
path: null,
|
||||
key: null,
|
||||
error: null,
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
Share.get(this.props.match.params.id)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
this.setState({
|
||||
loading: false,
|
||||
request_password: true
|
||||
});
|
||||
})
|
||||
.catch((res) => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
this._proofQuery(this.props.match.params.id).then(() => {
|
||||
if(this.refs.$input) {
|
||||
this.refs.$input.ref.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submitProof(e, type, value){
|
||||
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() {
|
||||
@ -45,46 +65,55 @@ export class SharePage extends React.Component {
|
||||
};
|
||||
};
|
||||
|
||||
if(this.state.loading === true){
|
||||
return ( <div> <Loader /> </div> );
|
||||
}
|
||||
let className = this.state.error ? "error rand-"+Math.random().toString() : "";
|
||||
|
||||
if(this.state.request_password === true){
|
||||
if(this.state.path !== null){
|
||||
if(filetype(this.state.path) === "directory"){
|
||||
return ( <Redirect to={`/files/?share=${this.state.share}`} /> );
|
||||
}else{
|
||||
return ( <Redirect to={`/view/${basename(this.state.path)}?share=${this.state.share}`} /> );
|
||||
}
|
||||
} else if (this.state.key === null){
|
||||
return (
|
||||
<Container maxWidth="350px">
|
||||
<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>
|
||||
<div style={marginTop()}>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}else if(this.state.request_username === true){
|
||||
} else if(this.state.key === "code"){
|
||||
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()}>
|
||||
<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="emphasis">OK</Button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
if(this.state.redirection !== null){
|
||||
if(this.state.redirection.slice(-1) === "/"){
|
||||
return ( <Redirect to={"/files" + this.state.redirection} /> );
|
||||
}else{
|
||||
return ( <Redirect to={"/view" + this.state.redirection} /> );
|
||||
}
|
||||
}else{
|
||||
return (
|
||||
<div className="error-page">
|
||||
<h1>Oops!</h1>
|
||||
@ -93,4 +122,3 @@ export class SharePage extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { Link, withRouter } from 'react-router-dom';
|
||||
import { Files } from '../../model/';
|
||||
import { sort } from '../../pages/filespage.helper.js';
|
||||
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';
|
||||
|
||||
|
||||
@ -25,7 +25,6 @@ export class Pager extends React.Component {
|
||||
componentDidMount(){
|
||||
this.setNavigation(this.props);
|
||||
window.addEventListener("keyup", this.navigate);
|
||||
|
||||
this.props.subscribe('media::next', () => {
|
||||
this.navigatePage(this.calculateNextPageNumber(this.state.n));
|
||||
});
|
||||
@ -35,7 +34,7 @@ export class Pager extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props){
|
||||
if(props.path === this.props.path){
|
||||
if(props.path !== this.props.path){
|
||||
this.setNavigation(props);
|
||||
}
|
||||
}
|
||||
@ -57,7 +56,8 @@ export class Pager extends React.Component {
|
||||
|
||||
navigatePage(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();
|
||||
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){
|
||||
@ -79,6 +79,10 @@ export class Pager extends React.Component {
|
||||
|
||||
setNavigation(props){
|
||||
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) => sort(f, settings_get('filespage_sort') || 'type'))
|
||||
.then((f) => findPosition(f, basename(props.path)))
|
||||
@ -87,7 +91,8 @@ export class Pager extends React.Component {
|
||||
files: res[0],
|
||||
n: res[1]
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const findPosition = (files, filename) => {
|
||||
let i;
|
||||
|
||||
@ -8,7 +8,7 @@ self.onmessage = function(message){
|
||||
if(current_search != null){
|
||||
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});
|
||||
}, null, () => {
|
||||
self.postMessage({type: "search::completed"})
|
||||
@ -16,7 +16,7 @@ self.onmessage = function(message){
|
||||
}
|
||||
}
|
||||
|
||||
function Search(path, keyword){
|
||||
function Search(key, keyword){
|
||||
let results = [];
|
||||
return new Observable((obs) => {
|
||||
obs.next(results);
|
||||
@ -32,7 +32,7 @@ function Search(path, keyword){
|
||||
results = results.concat(found);
|
||||
obs.next(results);
|
||||
}
|
||||
}, cache.FILE_PATH, path).then(() => {
|
||||
}, cache.FILE_PATH, [key[0], key[1]]).then(() => {
|
||||
obs.complete(results);
|
||||
});
|
||||
});
|
||||
|
||||
@ -15,6 +15,11 @@
|
||||
"level": "INFO",
|
||||
"telemetry": true
|
||||
},
|
||||
"smtp": {
|
||||
"addr": "smtp.gmail.com",
|
||||
"username": "mickael.kerjean@gmail.com",
|
||||
"password": "test"
|
||||
},
|
||||
"oauth": {
|
||||
"gdrive": {
|
||||
"client_id": "",
|
||||
|
||||
@ -36,6 +36,13 @@ type Config struct {
|
||||
Level string `json:"level"`
|
||||
Telemetry bool `json:"telemetry"`
|
||||
} `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 {
|
||||
Dropbox struct {
|
||||
ClientID string `json:"client_id"`
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
const (
|
||||
COOKIE_NAME = "auth"
|
||||
COOKIE_NAME_AUTH = "auth"
|
||||
COOKIE_NAME_PROOF = "proof"
|
||||
COOKIE_PATH = "/api/"
|
||||
)
|
||||
|
||||
@ -1,55 +1,141 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
mathrand "math/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func Encrypt(keystr string, text map[string]string) (string, error) {
|
||||
key := []byte(keystr)
|
||||
plaintext, err := json.Marshal(text)
|
||||
var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
func EncryptString(secret string, json string) (string, error) {
|
||||
d, err := compress([]byte(json))
|
||||
if err != nil {
|
||||
return "", NewError("json marshalling: "+err.Error(), 500)
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
d, err = encrypt([]byte(secret), d)
|
||||
if err != nil {
|
||||
return "", NewError("encryption issue (cipher): "+err.Error(), 500)
|
||||
return "", err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", NewError("encryption issue: "+err.Error(), 500)
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||
return base64.URLEncoding.EncodeToString(d), nil
|
||||
}
|
||||
|
||||
func Decrypt(keystr string, cryptoText string) (map[string]string, error) {
|
||||
var raw map[string]string
|
||||
|
||||
key := []byte(keystr)
|
||||
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)
|
||||
func DecryptString(secret string, data string) (string, error){
|
||||
d, err := base64.URLEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d, err = decrypt([]byte(secret), d)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d, err = decompress(d)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(d), nil
|
||||
}
|
||||
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
func Hash(str string) string {
|
||||
hasher := sha1.New()
|
||||
hasher.Write([]byte(str))
|
||||
return "sha1::" + base32.HexEncoding.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
json.Unmarshal(ciphertext, &raw)
|
||||
return raw, 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 {
|
||||
@ -63,7 +149,5 @@ func GenerateID(params map[string]string) string {
|
||||
p += "endpoint =>" + params["endpoint"]
|
||||
p += "bearer =>" + params["bearer"]
|
||||
p += "token =>" + params["token"]
|
||||
hasher := sha1.New()
|
||||
hasher.Write([]byte(p))
|
||||
return "sha1::" + base32.HexEncoding.EncodeToString(hasher.Sum(nil))
|
||||
return Hash(p)
|
||||
}
|
||||
|
||||
@ -5,18 +5,18 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncryptSomething(t *testing.T) {
|
||||
func TestEncryptString(t *testing.T) {
|
||||
key := "test|test|test|test|test"
|
||||
|
||||
d := make(map[string]string)
|
||||
d["foo"] = "bar"
|
||||
|
||||
str, err := Encrypt(key, d)
|
||||
text := "I'm some text"
|
||||
a, err := EncryptString(key, text)
|
||||
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.Equal(t, "bar", data["foo"])
|
||||
assert.Equal(t, b, text)
|
||||
|
||||
}
|
||||
|
||||
func TestIDGeneration(t *testing.T) {
|
||||
@ -32,3 +32,11 @@ func TestIDGeneration(t *testing.T) {
|
||||
assert.NotEqual(t, id1, id2)
|
||||
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"`
|
||||
CanMove *bool `json:"can_move,omitempty"`
|
||||
CanUpload *bool `json:"can_upload,omitempty"`
|
||||
CanDelete *bool `json:"can_delete,omitempty"`
|
||||
CanShare *bool `json:"can_share,omitempty"`
|
||||
Expire *time.Time `json:"-"`
|
||||
}
|
||||
|
||||
@ -1,19 +1,5 @@
|
||||
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 {
|
||||
return &t
|
||||
}
|
||||
@ -35,13 +21,13 @@ func NewBoolFromInterface(val interface{}) bool {
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
func NewIntpFromInterface(val interface{}) *int {
|
||||
func NewInt64pFromInterface(val interface{}) *int64 {
|
||||
switch val.(type) {
|
||||
case int:
|
||||
v := val.(int)
|
||||
case int64:
|
||||
v := val.(int64)
|
||||
return &v
|
||||
case float64:
|
||||
v := int(val.(float64))
|
||||
v := int64(val.(float64))
|
||||
return &v
|
||||
default: return nil
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package ctrl
|
||||
import (
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/mickael-kerjean/nuage/server/services"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
@ -18,6 +19,16 @@ type FileInfo struct {
|
||||
}
|
||||
|
||||
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"))
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
@ -30,7 +41,6 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
files := []FileInfo{}
|
||||
for _, entry := range entries {
|
||||
f := FileInfo{
|
||||
Name: entry.Name(),
|
||||
@ -48,10 +58,28 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
var perms *Metadata
|
||||
var perms *Metadata = &Metadata{}
|
||||
if obj, ok := ctx.Backend.(interface{ Meta(path string) *Metadata }); ok {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -62,6 +90,10 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
MaxAge: -1,
|
||||
Path: "/",
|
||||
})
|
||||
if model.CanRead(&ctx) == false {
|
||||
SendErrorResult(res, NewError("Permission denied", 403))
|
||||
return
|
||||
}
|
||||
|
||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||
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) {
|
||||
if model.CanEdit(&ctx) == false {
|
||||
SendErrorResult(res, NewError("Permission denied", 403))
|
||||
return
|
||||
}
|
||||
|
||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
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) {
|
||||
if model.CanEdit(&ctx) == false {
|
||||
SendErrorResult(res, NewError("Permission denied", 403))
|
||||
return
|
||||
}
|
||||
|
||||
from, err := pathBuilder(ctx, req.URL.Query().Get("from"))
|
||||
if err != nil {
|
||||
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) {
|
||||
if model.CanEdit(&ctx) == false {
|
||||
SendErrorResult(res, NewError("Permission denied", 403))
|
||||
return
|
||||
}
|
||||
|
||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
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) {
|
||||
if model.CanUpload(&ctx) == false {
|
||||
SendErrorResult(res, NewError("Permission denied", 403))
|
||||
return
|
||||
}
|
||||
|
||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
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) {
|
||||
if model.CanUpload(&ctx) == false {
|
||||
SendErrorResult(res, NewError("Permission denied", 403))
|
||||
return
|
||||
}
|
||||
|
||||
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/mickael-kerjean/mux"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
@ -63,13 +64,19 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
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 {
|
||||
SendErrorResult(res, NewError(err.Error(), 500))
|
||||
return
|
||||
}
|
||||
cookie := http.Cookie{
|
||||
Name: COOKIE_NAME,
|
||||
Name: COOKIE_NAME_AUTH,
|
||||
Value: obfuscate,
|
||||
MaxAge: 60 * 60 * 24 * 30,
|
||||
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) {
|
||||
cookie := http.Cookie{
|
||||
Name: COOKIE_NAME,
|
||||
Name: COOKIE_NAME_AUTH,
|
||||
Value: "",
|
||||
Path: COOKIE_PATH,
|
||||
MaxAge: -1,
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mickael-kerjean/mux"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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)
|
||||
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) {
|
||||
if model.CanShare(&ctx) == false {
|
||||
SendErrorResult(res, NewError("No permission", 403))
|
||||
return
|
||||
}
|
||||
|
||||
s := extractParams(req, &ctx)
|
||||
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 {
|
||||
SendErrorResult(res, err)
|
||||
@ -37,13 +92,91 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
SendSuccessResult(res, nil)
|
||||
}
|
||||
|
||||
func ShareGiveProof(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
// switch NewStringFromInterface(ctx.Body["type"]) {
|
||||
// case "password":
|
||||
// case "code": nil
|
||||
// case "email": nil
|
||||
// }
|
||||
SendSuccessResult(res, false)
|
||||
func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
var submittedProof model.Proof
|
||||
var verifiedProof []model.Proof
|
||||
var requiredProof []model.Proof
|
||||
var remainingProof []model.Proof
|
||||
var s model.Share
|
||||
|
||||
// 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) {
|
||||
@ -58,12 +191,13 @@ func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func extractParams(req *http.Request, ctx *App) model.Share {
|
||||
return model.Share{
|
||||
Auth: "",
|
||||
Id: NewStringFromInterface(mux.Vars(req)["id"]),
|
||||
Backend: NewStringFromInterface(GenerateID(ctx.Session)),
|
||||
Path: NewStringFromInterface(req.URL.Query().Get("path")),
|
||||
Password: NewStringpFromInterface(ctx.Body["password"]),
|
||||
Users: NewStringpFromInterface(ctx.Body["users"]),
|
||||
Expire: NewIntpFromInterface(ctx.Body["expire"]),
|
||||
Expire: NewInt64pFromInterface(ctx.Body["expire"]),
|
||||
Url: NewStringpFromInterface(ctx.Body["url"]),
|
||||
CanManageOwn: NewBoolFromInterface(ctx.Body["can_manage_own"]),
|
||||
CanShare: NewBoolFromInterface(ctx.Body["can_share"]),
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var DB *sql.DB
|
||||
@ -16,19 +17,33 @@ func init() {
|
||||
cachePath := filepath.Join(GetCurrentDir(), DBCachePath)
|
||||
os.MkdirAll(cachePath, os.ModePerm)
|
||||
var err error
|
||||
DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true")
|
||||
if err != nil {
|
||||
if DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true"); err != nil {
|
||||
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 err != nil {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -2,21 +2,38 @@ package model
|
||||
|
||||
import (
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/smtp"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 {
|
||||
Id string `json:"id"`
|
||||
Backend string `json:"-"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
Users *string `json:"users,omitempty"`
|
||||
Expire *int `json:"expire,omitempty"`
|
||||
Expire *int64 `json:"expire,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
CanShare bool `json:"can_share"`
|
||||
CanManageOwn bool `json:"can_manage_own"`
|
||||
@ -25,10 +42,27 @@ type Share struct {
|
||||
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) {
|
||||
p := Share{
|
||||
s.Id,
|
||||
s.Backend,
|
||||
"",
|
||||
s.Path,
|
||||
func(pass *string) *string{
|
||||
if pass != nil {
|
||||
@ -57,7 +91,7 @@ func(s *Share) UnmarshallJSON(b []byte) error {
|
||||
switch key {
|
||||
case "password": s.Password = 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 "can_share": s.CanShare = 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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := stmt.Query(p.Backend)
|
||||
rows, err := stmt.Query(p.Backend, p.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -91,26 +125,16 @@ func ShareList(p *Share) ([]Share, error) {
|
||||
}
|
||||
|
||||
func ShareGet(p *Share) error {
|
||||
if err := shareGet(p); err != nil {
|
||||
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 = ?")
|
||||
stmt, err := DB.Prepare("SELECT id, related_path, params, auth FROM share WHERE id = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
row := stmt.QueryRow(p.Id)
|
||||
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 {
|
||||
return NewError("No Result", 404)
|
||||
return NewError("Not Found", 404)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -123,7 +147,7 @@ func ShareUpsert(p *Share) error {
|
||||
if *p.Password == PASSWORD_DUMMY {
|
||||
var copy Share
|
||||
copy.Id = p.Id
|
||||
shareGet(©);
|
||||
ShareGet(©);
|
||||
p.Password = copy.Password
|
||||
} else {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
j, _ := json.Marshal(&struct {
|
||||
Password *string `json:"password,omitempty"`
|
||||
Users *string `json:"users,omitempty"`
|
||||
Expire *int `json:"expire,omitempty"`
|
||||
Expire *int64 `json:"expire,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
CanShare bool `json:"can_share"`
|
||||
CanManageOwn bool `json:"can_manage_own"`
|
||||
@ -171,7 +195,7 @@ func ShareUpsert(p *Share) error {
|
||||
CanWrite: p.CanWrite,
|
||||
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
|
||||
}
|
||||
|
||||
@ -183,3 +207,461 @@ func ShareDelete(p *Share) error {
|
||||
_, err = stmt.Exec(p.Id, p.Backend)
|
||||
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 (
|
||||
"testing"
|
||||
"github.com/mickael-kerjean/nuage/server/model"
|
||||
. "github.com/mickael-kerjean/nuage/server/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var shareObj = model.Share{
|
||||
var shareObj = Share{
|
||||
Backend: "foo",
|
||||
Id: "foo",
|
||||
Path: "/var/www/",
|
||||
@ -16,7 +15,7 @@ var shareObj = model.Share{
|
||||
CanRead: true,
|
||||
CanManageOwn: true,
|
||||
CanShare: true,
|
||||
Expire: NewInt(1537759505787),
|
||||
Expire: NewInt64(1537759505787),
|
||||
}
|
||||
|
||||
|
||||
@ -24,28 +23,28 @@ var shareObj = model.Share{
|
||||
//// UPSERT
|
||||
|
||||
func TestShareSimpleUpsert(t *testing.T) {
|
||||
err := model.ShareUpsert(&shareObj);
|
||||
err := ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShareMultipleUpsert(t *testing.T) {
|
||||
err := model.ShareUpsert(&shareObj);
|
||||
err := ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = model.ShareUpsert(&shareObj);
|
||||
err = ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = model.ShareGet(&shareObj)
|
||||
err = ShareGet(&shareObj)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShareUpsertIsProperlyInserted(t *testing.T) {
|
||||
err := model.ShareUpsert(&shareObj);
|
||||
err := ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err)
|
||||
|
||||
var obj model.Share
|
||||
var obj Share
|
||||
obj.Id = "foo"
|
||||
err = model.ShareGet(&obj)
|
||||
err = ShareGet(&obj)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, obj.Password)
|
||||
}
|
||||
@ -54,28 +53,28 @@ func TestShareUpsertIsProperlyInserted(t *testing.T) {
|
||||
//// get
|
||||
|
||||
func TestShareGetNonExisting(t *testing.T) {
|
||||
var s model.Share = shareObj
|
||||
var s Share = shareObj
|
||||
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")
|
||||
}
|
||||
|
||||
func TestShareGetExisting(t *testing.T) {
|
||||
err := model.ShareUpsert(&shareObj);
|
||||
err := ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err, "Upsert issue")
|
||||
|
||||
err = model.ShareGet(&shareObj);
|
||||
err = ShareGet(&shareObj);
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
|
||||
err := model.ShareUpsert(&shareObj);
|
||||
err := ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err, "Upsert issue")
|
||||
|
||||
var obj model.Share
|
||||
var obj Share
|
||||
obj.Id = "foo"
|
||||
obj.Backend = shareObj.Backend
|
||||
err = model.ShareGet(&obj);
|
||||
err = ShareGet(&obj);
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", obj.Id)
|
||||
assert.Equal(t, "/var/www/", obj.Path)
|
||||
@ -86,7 +85,6 @@ func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
|
||||
assert.Equal(t, false, obj.CanUpload)
|
||||
assert.Equal(t, "foo", obj.Backend)
|
||||
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) {
|
||||
// Initialise test
|
||||
err := model.ShareUpsert(&shareObj);
|
||||
err := ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err, "Upsert issue")
|
||||
|
||||
// Actual test
|
||||
list, err := model.ShareList(&shareObj)
|
||||
list, err := ShareList(&shareObj)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, list, 1)
|
||||
assert.NotNil(t, list[0].Password)
|
||||
@ -110,15 +108,49 @@ func TestShareListAll(t *testing.T) {
|
||||
|
||||
func TestShareDeleteShares(t *testing.T) {
|
||||
// Initialise test
|
||||
err := model.ShareUpsert(&shareObj);
|
||||
err := ShareUpsert(&shareObj);
|
||||
assert.NoError(t, err, "Upsert issue")
|
||||
err = model.ShareGet(&shareObj)
|
||||
err = ShareGet(&shareObj)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Actual Test
|
||||
err = model.ShareDelete(&shareObj);
|
||||
err = ShareDelete(&shareObj);
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = model.ShareGet(&shareObj)
|
||||
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,7 +33,7 @@ func Init(a *App) *http.Server {
|
||||
share.HandleFunc("/{id}", APIHandler(ShareGet, *a)).Methods("GET")
|
||||
share.HandleFunc("/{id}", APIHandler(ShareUpsert, *a)).Methods("POST")
|
||||
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
|
||||
r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET")
|
||||
|
||||
@ -17,6 +17,7 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
|
||||
start := time.Now()
|
||||
ctx.Body, _ = extractBody(req)
|
||||
ctx.Session, _ = extractSession(req, &ctx)
|
||||
|
||||
ctx.Backend, _ = extractBackend(req, &ctx)
|
||||
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) {
|
||||
cookie, err := req.Cookie(COOKIE_NAME)
|
||||
var str string
|
||||
var res map[string]string
|
||||
|
||||
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
|
||||
}
|
||||
return Decrypt(ctx.Config.General.SecretKey, cookie.Value)
|
||||
str = 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) {
|
||||
|
||||
@ -41,7 +41,7 @@ func ProcessFileBeforeSend(reader io.Reader, ctx *App, req *http.Request, res *h
|
||||
/////////////////////////
|
||||
// Specify transformation
|
||||
transform := &images.Transform{
|
||||
Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + RandomString(10)),
|
||||
Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + QuickString(10)),
|
||||
Size: 300,
|
||||
Crop: true,
|
||||
Quality: 50,
|
||||
|
||||
Reference in New Issue
Block a user