code base cleanup
4
.babelrc
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
'presets': ['react', 'es2015', 'stage-2'],
|
||||||
|
'plugins': ["transform-decorators-legacy", "syntax-dynamic-import"]
|
||||||
|
}
|
||||||
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
server/public/bundle.js
|
||||||
|
node_modules/
|
||||||
|
babel_cache/
|
||||||
|
.DS_Store
|
||||||
|
\#.*\#
|
||||||
|
.\#.*
|
||||||
50
README.org
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
* What is it about?
|
||||||
|
Call it a ftp client, an s3 viewer or a dropbox like web app, Nuage leverage your existing storage to help you manage your files in the cloud using any of the following protocols/platforms:
|
||||||
|
- FTP
|
||||||
|
- SFTP
|
||||||
|
- Webdav
|
||||||
|
- S3
|
||||||
|
- Dropbox
|
||||||
|
- Google Drive
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
* Features
|
||||||
|
- manage your files directly from your browser
|
||||||
|
- listen to music
|
||||||
|
- watch your videos
|
||||||
|
- show images
|
||||||
|
- work with multiple cloud providers and protocols
|
||||||
|
- upload files and folders
|
||||||
|
- mobile friendly
|
||||||
|
- emacs keybindings ;)
|
||||||
|
- works great with elinks and eww.
|
||||||
|
|
||||||
|
* What about my credentials?
|
||||||
|
Credentials are stored in your browser in a http only cookie encrypted using aes-256-ctr and aren't persist in the server disk at all.
|
||||||
|
The remember me feature rely on localstorage to store your credentials encrypted using aes-256-ctr.
|
||||||
|
|
||||||
|
Note that on the ftp and sftp session: connections aren't destroy on every request but are reused and kill after 2 minutes to make it feels faster and avoid reconnecting everytime you want to list some files.
|
||||||
|
|
||||||
|
|
||||||
|
* Install
|
||||||
|
It's a simple react app with node in the backend. Installation is easy with docker:
|
||||||
|
```
|
||||||
|
curl -X GET http://github.com/mickael-kerjean/nuage/master.zip > nuage.zip
|
||||||
|
unzip nuage.zip && cd nuage
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
That's it !
|
||||||
|
|
||||||
|
* Known Issues
|
||||||
|
- Webdav: the underlying library (webdav-fs) doesn't support stream which make it memory greedy if you try to upload or fetch large files.
|
||||||
|
- Google Drive: Google drive let you add multiple files with the same name in the same directory. You won't be able to see all those in Nuage as we assumed that all filenames in a directory are uniques.
|
||||||
|
|
||||||
|
* Motivation
|
||||||
|
I built this as a week end project initially to edit my org mode files on my mobile because I wasn't satisfied with any of the existing mobile client that try to force some predefined workflow or certain provider/protocols.
|
||||||
|
|
||||||
|
As I realise it soon, it doesn't have to be tight to org mode specifically and once I delivered the MVP, I just couldn't help myself to add more and more features and that's what I ended up with
|
||||||
|
|
||||||
|
* Credits
|
||||||
|
Icon from www.flaticon.com
|
||||||
23
config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// GOOGLE DRIVE
|
||||||
|
// 1) enable the api: https://console.developers.google.com/apis/api/drive.googleapis.com/overview
|
||||||
|
// 2) create credentials: https://console.developers.google.com/apis/credentials/oauthclient
|
||||||
|
|
||||||
|
// DROPBOX
|
||||||
|
// 1) create an third party app: https://www.dropbox.com/developers/apps/create
|
||||||
|
// -> dropbox api -> Full Dropbox -> whatever name you want ->
|
||||||
|
// -> set redirect URI to https://example.com/login ->
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
info: {
|
||||||
|
host: 'https://nuage.kerjean.me'
|
||||||
|
},
|
||||||
|
gdrive: {
|
||||||
|
redirectURI: "https://nuage.kerjean.me/login",
|
||||||
|
clientID: "client_id",
|
||||||
|
clientSecret: "client_secret"
|
||||||
|
},
|
||||||
|
dropbox: {
|
||||||
|
clientID: "client_id",
|
||||||
|
redirectURI: "https://nuage.kerjean.me/login"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: nuage
|
||||||
|
image: nuage
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SECRET_KEY=my_secret_key
|
||||||
|
ports:
|
||||||
|
- "10006:3000"
|
||||||
13
docker/img/Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
FROM node:wheezy
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
RUN cd /app/ && \
|
||||||
|
npm run build && \
|
||||||
|
ls ./ && \
|
||||||
|
echo $NODE_ENV
|
||||||
|
#rm -rf node_modules && \
|
||||||
|
#npm install && \
|
||||||
|
#npm run build
|
||||||
|
|
||||||
|
CMD ["node", "/app/server/index"]
|
||||||
72
package.json
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"name": "nuage",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack-dev-server --config webpack.config.js --progress --inline --hot --host 0.0.0.0",
|
||||||
|
"build": "NODE_ENV=production webpack -p",
|
||||||
|
"publish": "docker build -t nuage -f ./docker/img/Dockerfile . && docker tag nuage machines/nuage && docker push machines/nuage",
|
||||||
|
"start": "cd docker && docker-compose up",
|
||||||
|
"stop": "cd docker && docker-compose down"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.59.0",
|
||||||
|
"babel-polyfill": "^6.23.0",
|
||||||
|
"babel-preset-stage-2": "^6.24.1",
|
||||||
|
"body-parser": "^1.17.2",
|
||||||
|
"codemirror": "^5.26.0",
|
||||||
|
"cookie-parser": "^1.4.3",
|
||||||
|
"cors": "^2.8.3",
|
||||||
|
"crypto": "0.0.3",
|
||||||
|
"dropbox": "^2.5.3",
|
||||||
|
"ejs": "^2.5.6",
|
||||||
|
"express": "^4.15.3",
|
||||||
|
"express-winston": "^2.4.0",
|
||||||
|
"ftp": "^0.3.10",
|
||||||
|
"google-auth-library": "^0.10.0",
|
||||||
|
"googleapis": "^19.0.0",
|
||||||
|
"history": "^4.6.1",
|
||||||
|
"multiparty": "^4.1.3",
|
||||||
|
"node-ssh": "^4.2.2",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"pdfjs-dist": "^1.8.426",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react": "^15.3.2",
|
||||||
|
"react-dnd": "^2.4.0",
|
||||||
|
"react-dnd-html5-backend": "^2.4.1",
|
||||||
|
"react-dnd-touch-backend": "^0.3.11",
|
||||||
|
"react-dom": "^15.3.2",
|
||||||
|
"react-draggable": "^2.2.6",
|
||||||
|
"react-router": "^4.1.1",
|
||||||
|
"react-router-dom": "^4.1.1",
|
||||||
|
"request": "^2.81.0",
|
||||||
|
"rxjs": "^5.4.0",
|
||||||
|
"scp2": "^0.5.0",
|
||||||
|
"ssh2-sftp-client": "^1.1.0",
|
||||||
|
"stream-to-string": "^1.1.0",
|
||||||
|
"string-to-stream": "^1.1.0",
|
||||||
|
"video.js": "^5.19.2",
|
||||||
|
"wavesurfer.js": "^1.4.0",
|
||||||
|
"webdav-fs": "^1.0.0",
|
||||||
|
"winston": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.11.4",
|
||||||
|
"babel-core": "^6.13.2",
|
||||||
|
"babel-loader": "^6.2.10",
|
||||||
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
|
"babel-preset-es2015": "^6.13.2",
|
||||||
|
"babel-preset-react": "^6.11.1",
|
||||||
|
"babel-preset-stage-2": "^6.24.1",
|
||||||
|
"html-loader": "^0.4.5",
|
||||||
|
"html-webpack-plugin": "^2.28.0",
|
||||||
|
"http-server": "^0.9.0",
|
||||||
|
"nodemon": "^1.11.0",
|
||||||
|
"webpack": "^2.6.1",
|
||||||
|
"webpack-dev-server": "^2.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
server/bootstrap.js
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
var bodyParser = require('body-parser'),
|
||||||
|
cookieParser = require('cookie-parser'),
|
||||||
|
cors = require('cors'),
|
||||||
|
config = require('../config'),
|
||||||
|
express = require('express'),
|
||||||
|
winston = require('winston'),
|
||||||
|
expressWinston = require('express-winston');
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
if(process.env.NODE_ENV === 'production'){
|
||||||
|
app.use(expressWinston.logger({
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
json: false,
|
||||||
|
colorize: false
|
||||||
|
})
|
||||||
|
],
|
||||||
|
meta: false,
|
||||||
|
exitOnError: false,
|
||||||
|
msg: "HTTP {{res.statusCode}} {{req.method}} {{req.url}} {{res.responseTime}}ms",
|
||||||
|
expressFormat: true,
|
||||||
|
colorize: false,
|
||||||
|
ignoreRoute: function (req, res) { return false; }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
135
server/ctrl/files.js
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
var express = require('express'),
|
||||||
|
app = express.Router(),
|
||||||
|
crypto = require('../utils/crypto'),
|
||||||
|
Files = require('../model/files'),
|
||||||
|
multiparty = require('multiparty'),
|
||||||
|
mime = require('../utils/mimetype.js');
|
||||||
|
|
||||||
|
// list files
|
||||||
|
app.get('/ls', function(req, res){
|
||||||
|
let path = decodeURIComponent(req.query.path);
|
||||||
|
if(path){
|
||||||
|
Files
|
||||||
|
.ls(path, crypto.decrypt(req.cookies.auth))
|
||||||
|
.then(function(results){
|
||||||
|
res.send({status: 'ok', results: results});
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
res.send({status: 'error', message: err.message || 'cannot fetch files', trace: err})
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
res.send({status: 'error', message: 'unknown path'})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// get a file content
|
||||||
|
app.get('/cat', function(req, res){
|
||||||
|
let path = decodeURIComponent(req.query.path);
|
||||||
|
res.cookie('download', path, { maxAge: 1000 })
|
||||||
|
if(path){
|
||||||
|
Files.cat(path, crypto.decrypt(req.cookies.auth), res)
|
||||||
|
.then(function(stream){
|
||||||
|
res.set('Content-Type', mime.getMimeType(path));
|
||||||
|
stream.pipe(res);
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
res.send({status: 'error', message: err.message || 'couldn\t read the file', trace: err})
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
res.send({status: 'error', message: 'unknown path'})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// create/update a file
|
||||||
|
// https://github.com/pillarjs/multiparty
|
||||||
|
app.post('/cat', function(req, res){
|
||||||
|
var form = new multiparty.Form(),
|
||||||
|
path = decodeURIComponent(req.query.path);
|
||||||
|
|
||||||
|
if(path){
|
||||||
|
form.on('part', function(part) {
|
||||||
|
part.on('error', function(err){
|
||||||
|
console.log("ERROR", err);
|
||||||
|
res.send({status: 'error', message: 'internal error'})
|
||||||
|
});
|
||||||
|
|
||||||
|
Files.write(path, part, crypto.decrypt(req.cookies.auth))
|
||||||
|
.then(function(result){
|
||||||
|
res.send({status: 'ok'});
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
res.send({status: 'error', message: err.message || 'couldn\'t write the file', code: err.code})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
form.parse(req);
|
||||||
|
}else{
|
||||||
|
res.send({status: 'error', message: 'unknown path'})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// rename a file/directory
|
||||||
|
app.get('/mv', function(req, res){
|
||||||
|
let from = decodeURIComponent(req.query.from),
|
||||||
|
to = decodeURIComponent(req.query.to);
|
||||||
|
if(from && to){
|
||||||
|
Files.mv(from, to, crypto.decrypt(req.cookies.auth))
|
||||||
|
.then((message) => {
|
||||||
|
res.send({status: 'ok', result: message})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send({status: 'error', message: err.message || 'couldn\'t rename your file', trace: err})
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.send({status: 'error', message: 'unknown path'})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete a file/directory
|
||||||
|
app.get('/rm', function(req, res){
|
||||||
|
let path = decodeURIComponent(req.query.path);
|
||||||
|
if(path){
|
||||||
|
Files.rm(path, crypto.decrypt(req.cookies.auth))
|
||||||
|
.then((message) => {
|
||||||
|
res.send({status: 'ok', result: message})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send({status: 'error', message: err.message || 'couldn\'t delete your file', trace: err})
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.send({status: 'error', message: 'unknown path'})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// create a directory
|
||||||
|
app.get('/mkdir', function(req, res){
|
||||||
|
let path = decodeURIComponent(req.query.path);
|
||||||
|
if(path){
|
||||||
|
Files.mkdir(path, crypto.decrypt(req.cookies.auth))
|
||||||
|
.then((message) => {
|
||||||
|
res.send({status: 'ok', result: message})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send({status: 'error', message: err.message || 'couldn\'t create a directory', trace: err})
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.send({status: 'error', message: 'unknown path'})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/touch', function(req, res){
|
||||||
|
let path = decodeURIComponent(req.query.path);
|
||||||
|
if(path){
|
||||||
|
Files.touch(path, crypto.decrypt(req.cookies.auth))
|
||||||
|
.then((message) => {
|
||||||
|
res.send({status: 'ok', result: message})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send({status: 'error', message: err.message || 'couldn\'t create a file', trace: err})
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.send({status: 'error', message: 'unknown path'})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
54
server/ctrl/session.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
var express = require('express'),
|
||||||
|
app = express.Router(),
|
||||||
|
crypto = require('../utils/crypto'),
|
||||||
|
Session = require('../model/session'),
|
||||||
|
http = require('request-promise');
|
||||||
|
|
||||||
|
app.get('/', function(req, res){
|
||||||
|
let data = crypto.decrypt(req.cookies.auth);
|
||||||
|
if(data.type){
|
||||||
|
res.send({status: 'ok', result: true})
|
||||||
|
}else{
|
||||||
|
res.send({status: 'ok', result: false})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/', function(req, res){
|
||||||
|
Session.test(req.body)
|
||||||
|
.then((state) => {
|
||||||
|
let persist = {
|
||||||
|
type: req.body.type,
|
||||||
|
payload: state
|
||||||
|
};
|
||||||
|
res.cookie('auth', crypto.encrypt(persist), { maxAge: 365*24*60*60*1000, httpOnly: true });
|
||||||
|
res.send({status: 'ok', result: 'pong'});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
let message = function(err){
|
||||||
|
let t = 'could not establish a connection'
|
||||||
|
if(err.code){
|
||||||
|
t += ' ('+err.code+')'
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
res.send({status: 'error', message: message(err), code: err.code});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/', function(req, res){
|
||||||
|
res.clearCookie("auth");
|
||||||
|
res.send({status: 'ok'})
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/auth/:id', function(req, res){
|
||||||
|
Session.auth({type: req.params.id})
|
||||||
|
.then((url) => {
|
||||||
|
res.send({status: 'ok', result: url})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send({status: 'error', message: 'can\'t get authorization url', trace: err})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
17
server/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
var app = require('./bootstrap'),
|
||||||
|
express = require('express'),
|
||||||
|
filesRouter = require('./ctrl/files'),
|
||||||
|
sessionRouter = require('./ctrl/session');
|
||||||
|
|
||||||
|
app.get('/api/ping', function(req, res){ res.send('pong')})
|
||||||
|
app.use('/api/files', filesRouter)
|
||||||
|
app.use('/api/session', sessionRouter);
|
||||||
|
app.use('/', express.static(__dirname + '/public/'))
|
||||||
|
app.use('/*', function (req, res){
|
||||||
|
res.sendFile(__dirname + '/public/index.html')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(3000, function(err){
|
||||||
|
if(err){ console.log(err); }
|
||||||
|
else{ console.log("Running at Port 3000"); }
|
||||||
|
});
|
||||||
146
server/model/backend/dropbox.js
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
// https://www.dropbox.com/developers-v1/core/docs#oa2-authorize
|
||||||
|
var http = require('request-promise'),
|
||||||
|
http_stream = require('request'),
|
||||||
|
Path = require('path'),
|
||||||
|
config = require('../../../config'),
|
||||||
|
toString = require('stream-to-string');
|
||||||
|
|
||||||
|
|
||||||
|
function query(params, uri, method = 'GET', data){
|
||||||
|
let opts = {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer '+params.bearer,
|
||||||
|
},
|
||||||
|
uri: uri,
|
||||||
|
method: method
|
||||||
|
};
|
||||||
|
if(method === 'POST'){
|
||||||
|
opts.form = data;
|
||||||
|
}else if(method === 'PUT'){
|
||||||
|
opts.body = data;
|
||||||
|
opts.headers['Content-Length'] = data.length
|
||||||
|
}
|
||||||
|
return http(opts)
|
||||||
|
.then((res) => Promise.resolve(JSON.parse(res)))
|
||||||
|
.catch((res) => {
|
||||||
|
if(res && res.response && res.response.body){
|
||||||
|
return Promise.reject(res.response.body);
|
||||||
|
}else{
|
||||||
|
return Promise.reject(res);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function query_stream(params, uri, method = 'GET', data){
|
||||||
|
let opts = {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer '+params.bearer,
|
||||||
|
},
|
||||||
|
uri: uri,
|
||||||
|
method: method
|
||||||
|
};
|
||||||
|
if(method === 'POST'){
|
||||||
|
opts.form = data;
|
||||||
|
}else if(method === 'PUT'){
|
||||||
|
opts.body = data;
|
||||||
|
opts.headers['Content-Length'] = data.length
|
||||||
|
}
|
||||||
|
return Promise.resolve(http_stream(opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
auth: function(params){
|
||||||
|
let url = "https://www.dropbox.com/1/oauth2/authorize?client_id="+config.dropbox.clientID+"&response_type=token&redirect_uri="+config.dropbox.redirectURI+"&state=dropbox"
|
||||||
|
return Promise.resolve(url)
|
||||||
|
},
|
||||||
|
test: function(params){
|
||||||
|
return query(params, "https://api.dropboxapi.com/1/account/info")
|
||||||
|
.then((opts) => Promise.resolve(params))
|
||||||
|
.catch((err) => Promise.reject(err.response.body));
|
||||||
|
},
|
||||||
|
cat: function(path, params){
|
||||||
|
return query_stream(params, "https://content.dropboxapi.com/1/files/auto/"+path)
|
||||||
|
},
|
||||||
|
ls: function(path, params){
|
||||||
|
return query(params, "https://api.dropboxapi.com/1/metadata/auto/"+path)
|
||||||
|
.then((res) => {
|
||||||
|
let files = res.contents.map((file) => {
|
||||||
|
let tmp = {
|
||||||
|
size: file.bytes,
|
||||||
|
time: new Date(file.modified).getTime(),
|
||||||
|
type: file.is_dir? 'directory' : 'file',
|
||||||
|
name: file.path.split('/').slice(-1)[0]
|
||||||
|
};
|
||||||
|
if(file.read_only){
|
||||||
|
tmp.can_move = false;
|
||||||
|
tmp.can_delete = false;
|
||||||
|
}
|
||||||
|
return tmp;
|
||||||
|
});
|
||||||
|
if(res.read_only === true){
|
||||||
|
files.push({type: 'metadata', name: './', can_create_file: false, can_create_directory: false});
|
||||||
|
}
|
||||||
|
return Promise.resolve(files);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
write: function(path, content, params){
|
||||||
|
return process(path, content, params)
|
||||||
|
.then((res) => retryOnError(res, path, content, params, 5))
|
||||||
|
.then((res) => verifyDropbox(res, path, params, 10))
|
||||||
|
|
||||||
|
function process(path, content, params){
|
||||||
|
return query_stream(params, "https://content.dropboxapi.com/1/files_put/auto/"+path, "PUT", content)
|
||||||
|
.then(toString)
|
||||||
|
}
|
||||||
|
function retryOnError(body, path, content, params, n = 5){
|
||||||
|
body = JSON.parse(body);
|
||||||
|
|
||||||
|
if(body && body.error){
|
||||||
|
return sleep(Math.abs(5 - n) * 1000)
|
||||||
|
.then(() => process(path, content, params, n -1))
|
||||||
|
}else{
|
||||||
|
return Promise.resolve(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function verifyDropbox(keep, path, params, n = 10){
|
||||||
|
return sleep(Math.abs(10 - n) * 300)
|
||||||
|
.then(() => query(params, "https://api.dropboxapi.com/1/metadata/auto/"+Path.dirname(path)))
|
||||||
|
.then((res) => {
|
||||||
|
let found = res.contents.find((function(file){
|
||||||
|
return file.path === path? true : false
|
||||||
|
}));
|
||||||
|
if(found){
|
||||||
|
return Promise.resolve(keep)
|
||||||
|
}else{
|
||||||
|
if(n > 0){
|
||||||
|
return verifyDropbox(keep, path, params, n - 1)
|
||||||
|
}else{
|
||||||
|
return Promise.reject({message: 'dropbox didn\' create the file or was taking too long to do so', code: 'DROPBOX_WRITE_ERROR'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function sleep(t=1000, arg){
|
||||||
|
return new Promise((done) => {
|
||||||
|
setTimeout(function(){
|
||||||
|
done(arg);
|
||||||
|
}, t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rm: function(path, params){
|
||||||
|
return query(params, "https://api.dropboxapi.com/1/fileops/delete", "POST", {root: 'auto', path: path})
|
||||||
|
.then((res) => Promise.resolve('ok'))
|
||||||
|
},
|
||||||
|
mv: function(from, to, params){
|
||||||
|
return query(params, "https://api.dropboxapi.com/1/fileops/move", "POST", {root: 'auto', from_path: from, to_path: to})
|
||||||
|
.catch(err => Promise.reject({message: JSON.parse(err).error, code: "DROPBOX_MOVE"}))
|
||||||
|
},
|
||||||
|
mkdir: function(path, params){
|
||||||
|
return query(params, "https://api.dropboxapi.com/1/fileops/create_folder", "POST", {root: 'auto', path: path})
|
||||||
|
.then((res) => Promise.resolve('ok'))
|
||||||
|
},
|
||||||
|
touch: function(path, params){
|
||||||
|
return query(params, "https://content.dropboxapi.com/1/files_put/auto/"+path, "PUT", '')
|
||||||
|
.then((res) => Promise.resolve('ok'));
|
||||||
|
}
|
||||||
|
}
|
||||||
161
server/model/backend/ftp.js
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
var FtpClient = require("ftp");
|
||||||
|
|
||||||
|
// connections are reused to make things faster and avoid too much problems
|
||||||
|
const connections = {};
|
||||||
|
setInterval(() => {
|
||||||
|
for(let key in connections){
|
||||||
|
if(connections[key].date + (1000*120) < new Date().getTime()){
|
||||||
|
connections[key].conn.end();
|
||||||
|
delete connections[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function connect(params){
|
||||||
|
if(connections[JSON.stringify(params)]){
|
||||||
|
connections[JSON.stringify(params)].date = new Date().getTime();
|
||||||
|
return Promise.resolve(connections[JSON.stringify(params)].conn);
|
||||||
|
}else{
|
||||||
|
let c = new FtpClient();
|
||||||
|
c.connect({
|
||||||
|
host: params.hostname,
|
||||||
|
port: params.port || 21,
|
||||||
|
user: params.username,
|
||||||
|
password: params.password
|
||||||
|
});
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.on('ready', function(){
|
||||||
|
clearTimeout(timeout);
|
||||||
|
done(c);
|
||||||
|
connections[JSON.stringify(params)] = {
|
||||||
|
date: new Date().getTime(),
|
||||||
|
conn: c
|
||||||
|
}
|
||||||
|
});
|
||||||
|
c.on('error', function(error){
|
||||||
|
err(error)
|
||||||
|
})
|
||||||
|
// because of: https://github.com/mscdex/node-ftp/issues/187
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
err('timeout');
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
test: function(params){
|
||||||
|
return connect(params)
|
||||||
|
.then(() => Promise.resolve(params))
|
||||||
|
},
|
||||||
|
cat: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((c) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.get(path, function(error, stream) {
|
||||||
|
if (error){ err(error); }
|
||||||
|
else{ done(stream); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ls: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((c) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.list(path, function(error, list) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{
|
||||||
|
list = list
|
||||||
|
.map(el => {
|
||||||
|
return {
|
||||||
|
size: el.size,
|
||||||
|
time: new Date(el.date).getTime(),
|
||||||
|
name: el.name,
|
||||||
|
type: function(t){
|
||||||
|
if(t === '-'){
|
||||||
|
return 'file';
|
||||||
|
}else if(t === 'd'){
|
||||||
|
return 'directory';
|
||||||
|
}else if(t === 'l'){
|
||||||
|
return 'link';
|
||||||
|
}
|
||||||
|
}(el.type),
|
||||||
|
can_read: null,
|
||||||
|
can_write: null,
|
||||||
|
can_delete: null,
|
||||||
|
can_move: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(el => {
|
||||||
|
return el.name === '.' || el.name === '..' ? false : true
|
||||||
|
});
|
||||||
|
done(list);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
write: function(path, content, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((c) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.put(content, path, function(error){
|
||||||
|
if (error){ err(error)}
|
||||||
|
else{ done('ok'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
rm: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((c) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.delete(path, function(error){
|
||||||
|
if(error){
|
||||||
|
c.rmdir(path, true, function(error){
|
||||||
|
if(error) { err(error) }
|
||||||
|
else{ done('ok dir'); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else{ done('ok'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mv: function(from, to, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((c) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.rename(from, to, function(error){
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mkdir: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((c) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.mkdir(path, function(error){
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
touch: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((c) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
c.put(Buffer.from(''), path, function(error){
|
||||||
|
if (error){ err(error)}
|
||||||
|
else{ done('ok'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
486
server/model/backend/gdrive.js
Normal file
|
|
@ -0,0 +1,486 @@
|
||||||
|
// https://developers.google.com/drive/v3/web/quickstart/nodejs
|
||||||
|
// https://developers.google.com/apis-explorer/?hl=en_GB#p/drive/v3/
|
||||||
|
var google = require('googleapis'),
|
||||||
|
googleAuth = require('google-auth-library'),
|
||||||
|
config = require('../../../config'),
|
||||||
|
Stream = require('stream'),
|
||||||
|
Readable = require('stream').Readable;
|
||||||
|
|
||||||
|
var client = google.drive('v3');
|
||||||
|
|
||||||
|
|
||||||
|
function findMimeType(filename){
|
||||||
|
let ext = filename.split('.').slice(-1)[0];
|
||||||
|
let list = {
|
||||||
|
xls: 'application/vnd.ms-excel',
|
||||||
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
xml: 'text/xml',
|
||||||
|
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
||||||
|
csv: 'text/csv',
|
||||||
|
tmpl: 'text/plain',
|
||||||
|
org: 'text/plain',
|
||||||
|
md: 'text/plain',
|
||||||
|
pdf: 'application/pdf',
|
||||||
|
php: 'application/x-httpd-php',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
png: 'image/png',
|
||||||
|
gif: 'image/gif',
|
||||||
|
bmp: 'image/bmp',
|
||||||
|
txt: 'text/plain',
|
||||||
|
text: 'text/plain',
|
||||||
|
conf: 'text/plain',
|
||||||
|
log: 'text/plain',
|
||||||
|
doc: 'application/msword',
|
||||||
|
js: 'text/js',
|
||||||
|
swf: 'application/x-shockwave-flash',
|
||||||
|
mp3: 'audio/mpeg',
|
||||||
|
zip: 'application/zip',
|
||||||
|
rar: 'application/rar',
|
||||||
|
tar: 'application/tar',
|
||||||
|
arj: 'application/arj',
|
||||||
|
cab: 'application/cab',
|
||||||
|
html: 'text/html',
|
||||||
|
htm: 'text/html'
|
||||||
|
};
|
||||||
|
return list[ext] || 'application/octet-stream'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function decode(path){
|
||||||
|
let tmp = path.split('/');
|
||||||
|
let filename = tmp.pop() || null;
|
||||||
|
let space = tmp.splice(0,2)[1];
|
||||||
|
space = space? space.toLowerCase() : null;
|
||||||
|
return {
|
||||||
|
space: space || null,
|
||||||
|
name: filename,
|
||||||
|
parents: tmp,
|
||||||
|
full: filename === null ? tmp : [].concat(tmp, [filename])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findId(auth, folders, ids = []){
|
||||||
|
let name = folders.pop();
|
||||||
|
|
||||||
|
return search(auth, name, folders)
|
||||||
|
.then((files) => {
|
||||||
|
let solutions = findSolutions(files, ids);
|
||||||
|
let newCache = [].concat(solutions, ids);
|
||||||
|
// console.log("=====")
|
||||||
|
// console.log("SEARCH ON", name)
|
||||||
|
// console.log("FILES", files.map((file) => file.id))
|
||||||
|
// console.log("CACHE", ids)
|
||||||
|
// console.log("> SOLUTIONS => ",solutions)
|
||||||
|
if(solutions.length === 0){
|
||||||
|
return Promise.reject({message: 'this path doesn\'t exist', code: 'UNKNOWN_PATH'})
|
||||||
|
}else if(solutions.length === 1){
|
||||||
|
return Promise.resolve(findLast(solutions[0].id, ids));
|
||||||
|
}else{
|
||||||
|
return findId(auth, folders, newCache);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function search(_auth, _name, _folders){
|
||||||
|
if(_name === undefined){
|
||||||
|
return findRoot(_auth);
|
||||||
|
}else{
|
||||||
|
return findByName(_auth, _name, _folders.length + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRoot(auth){
|
||||||
|
return new Promise((_done,_err) => {
|
||||||
|
client.files.list({
|
||||||
|
auth: auth,
|
||||||
|
q: "'root' in parents",
|
||||||
|
pageSize: 1,
|
||||||
|
fields: "files(parents, id, name)"
|
||||||
|
}, function(error, response){
|
||||||
|
if(error){_err(error)}
|
||||||
|
else{
|
||||||
|
if(response.files.length > 0){
|
||||||
|
_done(response.files.map((file) => {
|
||||||
|
return {
|
||||||
|
level: 0,
|
||||||
|
id: file.parents[0],
|
||||||
|
name: 'root'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}else{
|
||||||
|
_done([{
|
||||||
|
level: 0,
|
||||||
|
id: 'root',
|
||||||
|
name: 'root'
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findByName(auth, name, _level){
|
||||||
|
return new Promise((_done,_err) => {
|
||||||
|
client.files.list({
|
||||||
|
auth: auth,
|
||||||
|
q: "name = '"+name+"'",
|
||||||
|
pageSize: 500,
|
||||||
|
fields: "files(parents, id, name)"
|
||||||
|
}, function(error, response){
|
||||||
|
if(error){_err(error)}
|
||||||
|
else{
|
||||||
|
_done(response.files.map((file) => {
|
||||||
|
file.level = _level
|
||||||
|
return file;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLast(id, cache){
|
||||||
|
if(id === 'root'){ return id; }
|
||||||
|
for(let i=0, l=cache.length; i<l; i++){
|
||||||
|
if(id === cache[i].parents[0]){
|
||||||
|
return findLast(cache[i].id, cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSolutions(newFiles, cache){
|
||||||
|
return newFiles.filter((newFile) => {
|
||||||
|
if(cache.length === 0){ return true;}
|
||||||
|
else{
|
||||||
|
for(let i=0, j=cache.length; i<j; i++){
|
||||||
|
if(newFile.id === cache[i].parents[0] && (newFile.level + 1) === cache[i].level){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function authorize(params){
|
||||||
|
var auth = new googleAuth(),
|
||||||
|
client_id = config.gdrive.clientID,
|
||||||
|
client_secret = config.gdrive.clientSecret,
|
||||||
|
redirect_uri = config.gdrive.redirectURI;
|
||||||
|
|
||||||
|
var oauth2Client = new auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
|
return Promise.resolve(oauth2Client);
|
||||||
|
}
|
||||||
|
function connect(params){
|
||||||
|
return authorize(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
if(params && params.access_token){
|
||||||
|
auth.credentials = params;
|
||||||
|
done(auth);
|
||||||
|
}else if(params && params.code){
|
||||||
|
auth.getToken(params.code, function(error, token) {
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{
|
||||||
|
auth.credentials = token;
|
||||||
|
done(auth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
err({message: 'can\'t connect without auth code or token', code: 'INVALID_CONNECTION'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(auth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
auth: function(params){
|
||||||
|
return authorize()
|
||||||
|
.then((auth) => {
|
||||||
|
return Promise.resolve(auth.generateAuthUrl({
|
||||||
|
access_type: 'online',
|
||||||
|
scope: [
|
||||||
|
"https://www.googleapis.com/auth/drive",
|
||||||
|
"https://www.googleapis.com/auth/drive.photos.readonly"
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
},
|
||||||
|
test: function(params){
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.list({
|
||||||
|
auth: auth,
|
||||||
|
q: "'root' in parents AND mimeType = 'application/vnd.google-apps.folder'",
|
||||||
|
pageSize: 5,
|
||||||
|
fields: "files(parents)"
|
||||||
|
}, function(error, response) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done(auth.credentials) }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cat: function(path, params){
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return findId(auth, path.full)
|
||||||
|
.then((id) => fileInfo(auth, id))
|
||||||
|
.then((file) => {
|
||||||
|
if(/application\/vnd.google-apps/.test(file.mimeType)){
|
||||||
|
let type = 'text/plain';
|
||||||
|
if(file.mimeType === 'application/vnd.google-apps.spreadsheet'){
|
||||||
|
type = 'text/csv';
|
||||||
|
}
|
||||||
|
return exporter(auth, file.id, type);
|
||||||
|
}else{
|
||||||
|
console.log("DOWNLOAD",file)
|
||||||
|
return download(auth, file.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function fileInfo(auth, id){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.get({
|
||||||
|
auth: auth,
|
||||||
|
fileId: id
|
||||||
|
},function(error, response){
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{ done(response); }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function download(auth, id){
|
||||||
|
var content = '';
|
||||||
|
return Promise.resolve(client.files.get({
|
||||||
|
auth: auth,
|
||||||
|
fileId: id,
|
||||||
|
alt: 'media'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function exporter(auth, id, type){
|
||||||
|
var content = '';
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
done(client.files.export({
|
||||||
|
auth: auth,
|
||||||
|
fileId: id,
|
||||||
|
mimeType: type
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ls: function(_path, params){
|
||||||
|
path = decode(_path);
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
if(path.space === null){
|
||||||
|
return Promise.resolve([
|
||||||
|
{type: 'metadata', name: './', can_create_file: false, can_create_directory: false},
|
||||||
|
{type: 'directory', name: 'Drive', can_read: true, can_write: false, can_delete: false, can_move: false},
|
||||||
|
{type: 'directory', name: 'Photos', can_read: true, can_write: false, can_delete: false, can_move: false}
|
||||||
|
]);
|
||||||
|
}else{
|
||||||
|
if(path.space === 'photos'){
|
||||||
|
return findPhotos(auth)
|
||||||
|
.then(parse)
|
||||||
|
.then((files) => {
|
||||||
|
files.push({type: 'metadata', name: '.', can_create_file: false, can_create_directory: false})
|
||||||
|
return Promise.resolve(files)
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
return findId(auth, JSON.parse(JSON.stringify(path.parents)))
|
||||||
|
.then((id) => {
|
||||||
|
return findDrive(auth, id)
|
||||||
|
.then(parse)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function findPhotos(auth){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.list({
|
||||||
|
spaces: path.space,
|
||||||
|
auth: auth,
|
||||||
|
q: "trashed = false",
|
||||||
|
pageSize: 500,
|
||||||
|
fields: "files(id,mimeType,modifiedTime,name,size)"
|
||||||
|
}, function(error, response) {
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{ done(response.files); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function findDrive(auth, id){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.list({
|
||||||
|
spaces: path.space,
|
||||||
|
auth: auth,
|
||||||
|
q: "'"+id+"' in parents AND trashed = false",
|
||||||
|
pageSize: 500,
|
||||||
|
fields: "files(id,mimeType,modifiedTime,name,size)"
|
||||||
|
}, function(error, response) {
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{ done(response.files); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function parse(files){
|
||||||
|
return Promise.resolve(files.map((file) => {
|
||||||
|
return {
|
||||||
|
type: file.mimeType === 'application/vnd.google-apps.folder'? 'directory' : 'file',
|
||||||
|
name: file.name,
|
||||||
|
size: file.hasOwnProperty('size')? Number(file.size) : 0,
|
||||||
|
time: new Date(file.modifiedTime).getTime()
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
write: function(path, content, params){
|
||||||
|
path = decode(path);
|
||||||
|
var readable = new Stream.Readable();
|
||||||
|
readable.push(content);
|
||||||
|
readable.push(null);
|
||||||
|
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return findId(auth, path.full)
|
||||||
|
.then((id) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.update({
|
||||||
|
auth: auth,
|
||||||
|
fileId: id,
|
||||||
|
fields: 'id',
|
||||||
|
media: {
|
||||||
|
mimeType: findMimeType(path.name),
|
||||||
|
body: readable
|
||||||
|
}
|
||||||
|
}, function(error){
|
||||||
|
if(error) {err(error) }
|
||||||
|
else{ done('ok'); }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
rm: function(path, params){
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return findId(auth, path.full)
|
||||||
|
.then((id) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.delete({
|
||||||
|
fileId: id,
|
||||||
|
auth: auth
|
||||||
|
}, function(error){
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{ done('ok'); }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mv: function(from, to, params){
|
||||||
|
from = decode(from);
|
||||||
|
to = decode(to);
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return Promise.all([findId(auth, from.full), findId(auth, from.parents), findId(auth, to.parents)])
|
||||||
|
.then((res) => process(auth, res))
|
||||||
|
//.then(wait)
|
||||||
|
});
|
||||||
|
|
||||||
|
function wait(res){
|
||||||
|
return new Promise((done) => {
|
||||||
|
setTimeout(function(){
|
||||||
|
done(res);
|
||||||
|
}, 500)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function process(auth, res){
|
||||||
|
let fileId = res[0],
|
||||||
|
srcId = res[1],
|
||||||
|
destId = res[2];
|
||||||
|
let fields = 'id';
|
||||||
|
let params = {fileId, fileId, auth: auth}
|
||||||
|
if(destId !== srcId){
|
||||||
|
fields += ', parents'
|
||||||
|
params.addParents = destId;
|
||||||
|
params.removeParents = srcId;
|
||||||
|
}
|
||||||
|
if(to.name !== null && from.name !== null && from.name !== to.name ){
|
||||||
|
fields += 'name';
|
||||||
|
params.resource = {
|
||||||
|
name: to.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.update(params, function(error, response){
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mkdir: function(path, params){
|
||||||
|
path = decode(path);
|
||||||
|
let name = path.parents.pop();
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return findId(auth, path.parents)
|
||||||
|
.then((folder) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
done('ok');
|
||||||
|
client.files.create({
|
||||||
|
fields: 'id',
|
||||||
|
auth: auth,
|
||||||
|
resource: {
|
||||||
|
name: name,
|
||||||
|
parents: [folder],
|
||||||
|
mimeType: 'application/vnd.google-apps.folder'
|
||||||
|
}
|
||||||
|
}, function(error){
|
||||||
|
if(error) {err(error) }
|
||||||
|
else{ done('ok'); }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
touch: function(path, params){
|
||||||
|
path = decode(path);
|
||||||
|
var readable = new Stream.Readable();
|
||||||
|
readable.push('');
|
||||||
|
readable.push(null);
|
||||||
|
|
||||||
|
return connect(params)
|
||||||
|
.then((auth) => {
|
||||||
|
return findId(auth, path.parents)
|
||||||
|
.then((folder) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
client.files.create({
|
||||||
|
auth: auth,
|
||||||
|
fields: 'id',
|
||||||
|
media: {
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
body: readable
|
||||||
|
},
|
||||||
|
resource: {
|
||||||
|
name: path.name,
|
||||||
|
parents: [folder]
|
||||||
|
}
|
||||||
|
}, function(error){
|
||||||
|
if(error) {err(error) }
|
||||||
|
else{ done('ok'); }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
241
server/model/backend/s3.js
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
// https://www.npmjs.com/package/aws-sdk
|
||||||
|
var AWS = require('aws-sdk');
|
||||||
|
|
||||||
|
|
||||||
|
function decode(path){
|
||||||
|
let tmp = path.split('/');
|
||||||
|
return {
|
||||||
|
bucket: tmp.splice(0, 2)[1] || null,
|
||||||
|
path: tmp.join('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect(params){
|
||||||
|
var s3 = new AWS.S3({
|
||||||
|
apiVersion: '2006-03-01',
|
||||||
|
accessKeyId: params.access_key_id,
|
||||||
|
secretAccessKey: params.secret_access_key,
|
||||||
|
region: params.region,
|
||||||
|
sslEnabled: true
|
||||||
|
});
|
||||||
|
return Promise.resolve(s3);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
test: function(params){
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.listBuckets(function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done(params) }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cat: function(path, params, res){
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
return Promise.resolve(s3.getObject({
|
||||||
|
Bucket: path.bucket,
|
||||||
|
Key: path.path
|
||||||
|
}).on('httpHeaders', function (statusCode, headers) {
|
||||||
|
res.set('content-type', headers['content-type']);
|
||||||
|
res.set('content-length', headers['content-length']);
|
||||||
|
res.set('last-modified', headers['last-modified']);
|
||||||
|
}).createReadStream())
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ls: function(path, params){
|
||||||
|
if(/\/$/.test(path) === false) path += '/';
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
if(path.bucket === null){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.listBuckets(function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{
|
||||||
|
let buckets = data.Buckets.map((bucket) => {
|
||||||
|
return {
|
||||||
|
name: bucket.Name,
|
||||||
|
type: 'bucket',
|
||||||
|
time: new Date(bucket.CreationDate).getTime(),
|
||||||
|
can_read: true,
|
||||||
|
can_delete: true,
|
||||||
|
can_move: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
buckets.push({type: 'metadata', name: './', can_create_file: false, can_create_directory: true});
|
||||||
|
done(buckets)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.listObjects({
|
||||||
|
Bucket: path.bucket,
|
||||||
|
Prefix: path.path,
|
||||||
|
Delimiter: '/'
|
||||||
|
}, function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{
|
||||||
|
let content = data.Contents
|
||||||
|
.filter((file) => {
|
||||||
|
return file.Key === path.path? false : true;
|
||||||
|
})
|
||||||
|
.map((file) => {
|
||||||
|
return {
|
||||||
|
type: 'file',
|
||||||
|
size: file.Size,
|
||||||
|
time: new Date(file.LastModified).getTime(),
|
||||||
|
name: file.Key.split('/').pop()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let folders = data.CommonPrefixes.map((prefix) => {
|
||||||
|
return {
|
||||||
|
type: 'directory',
|
||||||
|
size: 0,
|
||||||
|
time: null,
|
||||||
|
name: prefix.Prefix.split('/').slice(-2)[0]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return done([].concat(folders, content));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
write: function(path, stream, params){
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.upload({
|
||||||
|
Bucket: path.bucket,
|
||||||
|
Key: path.path,
|
||||||
|
Body: stream,
|
||||||
|
ContentLength: stream.byteCount
|
||||||
|
}, function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{
|
||||||
|
done('ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rm: function(path, params){
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.listObjects({
|
||||||
|
Bucket: path.bucket,
|
||||||
|
Prefix: path.path
|
||||||
|
}, function(error, obj){
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{
|
||||||
|
Promise.all(obj.Contents.map((file) => {
|
||||||
|
return deleteObject(s3, path.bucket, file.Key)
|
||||||
|
})).then(function(){
|
||||||
|
if(path.path === ''){
|
||||||
|
s3.deleteBucket({
|
||||||
|
Bucket: path.bucket
|
||||||
|
}, function(error){
|
||||||
|
if(error){ err(error)}
|
||||||
|
else{ done('ok'); }
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
done('ok');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteObject(s3, bucket, key){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.deleteObject({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: key
|
||||||
|
}, function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mv: function(from, to, params){
|
||||||
|
from = decode(from);
|
||||||
|
to = decode(to);
|
||||||
|
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.copyObject({
|
||||||
|
Bucket: to.bucket,
|
||||||
|
CopySource: from.bucket+'/'+from.path,
|
||||||
|
Key: to.path
|
||||||
|
}, function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{
|
||||||
|
s3.deleteObject({
|
||||||
|
Bucket: from.bucket,
|
||||||
|
Key: from.path
|
||||||
|
}, function(error){
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{
|
||||||
|
done('ok');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mkdir: function(path, params){
|
||||||
|
if(/\/$/.test(path) === false) path += '/';
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
if(path.path === ''){
|
||||||
|
s3.createBucket({
|
||||||
|
Bucket: path.bucket
|
||||||
|
}, function(error, data){
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
s3.putObject({
|
||||||
|
Bucket: path.bucket,
|
||||||
|
Key: path.path
|
||||||
|
}, function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
touch: function(path, params){
|
||||||
|
path = decode(path);
|
||||||
|
return connect(params)
|
||||||
|
.then((s3) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
s3.putObject({
|
||||||
|
Bucket: path.bucket,
|
||||||
|
Key: path.path,
|
||||||
|
Body: ''
|
||||||
|
}, function(error, data) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
89
server/model/backend/sftp.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
var Client = require('ssh2-sftp-client');
|
||||||
|
|
||||||
|
const connections = {};
|
||||||
|
setInterval(() => {
|
||||||
|
for(let key in connections){
|
||||||
|
if(connections[key].date + (1000*120) < new Date().getTime()){
|
||||||
|
connections[key].conn.end();
|
||||||
|
delete connections[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
|
||||||
|
function connect(params){
|
||||||
|
if(connections[JSON.stringify(params)]){
|
||||||
|
connections[JSON.stringify(params)].date = new Date().getTime();
|
||||||
|
return Promise.resolve(connections[JSON.stringify(params)].conn);
|
||||||
|
}else{
|
||||||
|
let sftp = new Client();
|
||||||
|
let opts = {host: params.host, port: params.port || 22, username: params.username};
|
||||||
|
if(params.hasOwnProperty('private_key') && params['private_key']){
|
||||||
|
opts.privateKey = params['private_key']
|
||||||
|
}else{
|
||||||
|
opts.password = params['password'];
|
||||||
|
}
|
||||||
|
return sftp.connect(opts).then((res) => {
|
||||||
|
connections[JSON.stringify(params)] = {
|
||||||
|
date: new Date().getTime(),
|
||||||
|
conn: sftp
|
||||||
|
}
|
||||||
|
return Promise.resolve(sftp)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = {
|
||||||
|
test: function(params){
|
||||||
|
return connect(params)
|
||||||
|
.then(() => Promise.resolve(params))
|
||||||
|
},
|
||||||
|
cat: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((sftp) => sftp.get(path, false, null));
|
||||||
|
},
|
||||||
|
ls: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((sftp) => sftp.list(path))
|
||||||
|
.then((res) => {
|
||||||
|
return Promise.resolve(res.map((file) => {
|
||||||
|
return {
|
||||||
|
type: function(type){
|
||||||
|
if(type === 'd'){
|
||||||
|
return 'directory'
|
||||||
|
}else if(type === 'l'){
|
||||||
|
return 'link';
|
||||||
|
}else if(type === '-'){
|
||||||
|
return 'file';
|
||||||
|
}else{
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}(file.type),
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
time: file.modifyTime
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
write: function(path, content, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((sftp) => sftp.put(content, path))
|
||||||
|
},
|
||||||
|
rm: function(path, params){ //TODO recursive
|
||||||
|
return connect(params)
|
||||||
|
.then((sftp) => {
|
||||||
|
return sftp.delete(path)
|
||||||
|
.catch((err) => {
|
||||||
|
return sftp.rmdir(path, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mkdir: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((sftp) => sftp.mkdir(path, false))
|
||||||
|
},
|
||||||
|
touch: function(path, params){
|
||||||
|
return connect(params)
|
||||||
|
.then((sftp) => sftp.put(Buffer.from(''), path))
|
||||||
|
}
|
||||||
|
}
|
||||||
123
server/model/backend/webdav.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
var fs = require("webdav-fs");
|
||||||
|
var Readable = require('stream').Readable;
|
||||||
|
var toString = require('stream-to-string');
|
||||||
|
|
||||||
|
function connect(params){
|
||||||
|
return fs(
|
||||||
|
params.url,
|
||||||
|
params.username,
|
||||||
|
params.password
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(path){
|
||||||
|
return path
|
||||||
|
.split('/')
|
||||||
|
.map(function(link){
|
||||||
|
return encodeURIComponent(link);
|
||||||
|
})
|
||||||
|
.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
test: function(params){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
connect(params).readFile('/', function(error, res){
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done(params) }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cat: function(path, params){
|
||||||
|
path = encode(path);
|
||||||
|
return new Promise(function(done, err){
|
||||||
|
//path.replace(/\#/g, '%23')
|
||||||
|
connect(params).readFile(path, 'binary', function(error, res){
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{
|
||||||
|
var stream = new Readable();
|
||||||
|
stream.push(res);
|
||||||
|
stream.push(null);
|
||||||
|
done(stream);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ls: function(path, params){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
//path = encode(path);
|
||||||
|
//console.log(path)
|
||||||
|
connect(params).readdir(path, function(error, contents) {
|
||||||
|
if (!error) {
|
||||||
|
done(contents.map((content) => {
|
||||||
|
return {
|
||||||
|
name: content.name,
|
||||||
|
type: function(cont){
|
||||||
|
if(cont.isDirectory()){
|
||||||
|
return 'directory';
|
||||||
|
}else if(cont.isFile()){
|
||||||
|
return 'file'
|
||||||
|
}else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}(content),
|
||||||
|
time: content.mtime,
|
||||||
|
size: content.size
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
err(error);
|
||||||
|
}
|
||||||
|
}, 'stat');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
write: function(path, content, params){
|
||||||
|
path = encode(path);
|
||||||
|
return toString(content)
|
||||||
|
.then((content) => {
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
connect(params).writeFile(path, content, function(error) {
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{ done('done'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rm: function(path, params){
|
||||||
|
path = encode(path);
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
connect(params).unlink(path, function (error) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mv: function(from, to, params){
|
||||||
|
from = encode(from);
|
||||||
|
to = encode(to);
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
connect(params).rename(from, to, function (error) {
|
||||||
|
if(error){ err(error) }
|
||||||
|
else{ done('ok') }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mkdir: function(path, params){
|
||||||
|
path = encode(path);
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
connect(params).mkdir(path, function(error) {
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{ done('done'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
touch: function(path, params){
|
||||||
|
path = encode(path);
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
connect(params).writeFile(path, '', function(error) {
|
||||||
|
if(error){ err(error); }
|
||||||
|
else{ done('done'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
99
server/model/files.js
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
var backend = {
|
||||||
|
ftp: require('./backend/ftp'),
|
||||||
|
sftp: require('./backend/sftp'),
|
||||||
|
webdav: require('./backend/webdav'),
|
||||||
|
dropbox: require('./backend/dropbox'),
|
||||||
|
gdrive: require('./backend/gdrive'),
|
||||||
|
s3: require('./backend/s3')
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.cat = function(path, params, res){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].cat === 'function'){
|
||||||
|
return backend[params.type].cat(path, params.payload, res);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.write = function(path, content, params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].write === 'function'){
|
||||||
|
return backend[params.type].write(path, content, params.payload);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.ls = function(path, params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].ls === 'function'){
|
||||||
|
return backend[params.type].ls(path, params.payload);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.mv = function(from, to, params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].mv === 'function'){
|
||||||
|
return backend[params.type].mv(from, to, params.payload);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.rm = function(path, params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].rm === 'function'){
|
||||||
|
return backend[params.type].rm(path, params.payload);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.mkdir = function(path, params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].mkdir === 'function'){
|
||||||
|
return backend[params.type].mkdir(path, params.payload);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.touch = function(path, params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].touch === 'function'){
|
||||||
|
return backend[params.type].touch(path, params.payload);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function error(message){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
err(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
39
server/model/session.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
var backend = {
|
||||||
|
ftp: require('./backend/ftp'),
|
||||||
|
sftp: require('./backend/sftp'),
|
||||||
|
webdav: require('./backend/webdav'),
|
||||||
|
dropbox: require('./backend/dropbox'),
|
||||||
|
gdrive: require('./backend/gdrive'),
|
||||||
|
s3: require('./backend/s3')
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.test = function(params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].test === 'function'){
|
||||||
|
return backend[params.type].test(params);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.auth = function(params){
|
||||||
|
try{
|
||||||
|
if(backend[params.type] && typeof backend[params.type].auth === 'function'){
|
||||||
|
return backend[params.type].auth(params);
|
||||||
|
}else{
|
||||||
|
return error('not implemented');
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
err(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
378
server/public/css/codemirror.css
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
/* SEARCH */
|
||||||
|
.CodeMirror-dialog {
|
||||||
|
position: fixed;
|
||||||
|
left: 0; right: 0;
|
||||||
|
background: #525659;
|
||||||
|
z-index: 15;
|
||||||
|
padding: 5px .8em;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #e2e2e2;
|
||||||
|
box-shadow: 2px 2px 2px rgba(0,0,0,0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-dialog-top {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-dialog-bottom {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-dialog input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
width: 20em;
|
||||||
|
color: white;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-dialog button {
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* BASICS */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
/* Set height, width, borders, and global font properties here */
|
||||||
|
font-family: monospace;
|
||||||
|
height: 100%;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PADDING */
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
padding: 4px 0; /* Vertical padding around content */
|
||||||
|
}
|
||||||
|
.CodeMirror pre {
|
||||||
|
padding: 0 4px; /* Horizontal padding of content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||||
|
background-color: white; /* The little square between H and V scrollbars */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GUTTER */
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumbers {}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
padding: 0 3px 0 5px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: right;
|
||||||
|
color: #999;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-guttermarker { color: black; }
|
||||||
|
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||||
|
|
||||||
|
/* CURSOR */
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: none;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
/* Shown when moving in bi-directional text */
|
||||||
|
.CodeMirror div.CodeMirror-secondarycursor {
|
||||||
|
border-left: 1px solid silver;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor .CodeMirror-cursor {
|
||||||
|
width: auto;
|
||||||
|
border: 0 !important;
|
||||||
|
background: #7e7;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor div.CodeMirror-cursors {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-animate-fat-cursor {
|
||||||
|
width: auto;
|
||||||
|
border: 0;
|
||||||
|
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||||
|
-moz-animation: blink 1.06s steps(1) infinite;
|
||||||
|
animation: blink 1.06s steps(1) infinite;
|
||||||
|
background-color: #7e7;
|
||||||
|
}
|
||||||
|
@-moz-keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
@keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Can style cursor different in overwrite (non-insert) mode */
|
||||||
|
.CodeMirror-overwrite .CodeMirror-cursor {}
|
||||||
|
|
||||||
|
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||||
|
|
||||||
|
.CodeMirror-rulers {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: -50px; bottom: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.CodeMirror-ruler {
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEFAULT THEME */
|
||||||
|
|
||||||
|
.cm-s-default .cm-header {color: #3E7AA6;}
|
||||||
|
.cm-s-default .cm-link{color: #555!important;}
|
||||||
|
.cm-s-default .cm-url{color: #555!important;}
|
||||||
|
.cm-s-default .cm-quote {color: #090;}
|
||||||
|
.cm-negative {color: #d44;}
|
||||||
|
.cm-positive {color: #292;}
|
||||||
|
.cm-header, .cm-strong {font-weight: bold;}
|
||||||
|
.cm-em {font-style: italic;}
|
||||||
|
.cm-link {text-decoration: underline;}
|
||||||
|
.cm-strikethrough {text-decoration: line-through;}
|
||||||
|
|
||||||
|
.cm-s-default .cm-keyword {color: #3E7AA6;}
|
||||||
|
.cm-s-default .cm-atom {color: #219;}
|
||||||
|
.cm-s-default .cm-number {color: #164;}
|
||||||
|
.cm-s-default .cm-def {color: #00f;}
|
||||||
|
.cm-s-default .cm-variable,
|
||||||
|
.cm-s-default .cm-punctuation,
|
||||||
|
.cm-s-default .cm-property,
|
||||||
|
.cm-s-default .cm-operator {}
|
||||||
|
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||||
|
.cm-s-default .cm-variable-3 {color: #085;}
|
||||||
|
.cm-s-default .cm-comment {color: #6f6f6f;}
|
||||||
|
.cm-s-default .cm-string {color: #a11;}
|
||||||
|
.cm-s-default .cm-string-2 {color: #f50;}
|
||||||
|
.cm-s-default .cm-meta {color: #555;}
|
||||||
|
.cm-s-default .cm-qualifier {color: #555;}
|
||||||
|
.cm-s-default .cm-builtin {color: #30a;}
|
||||||
|
.cm-s-default .cm-bracket {color: #997;}
|
||||||
|
.cm-s-default .cm-tag {color: #170;}
|
||||||
|
.cm-s-default .cm-attribute {color: #00c;}
|
||||||
|
.cm-s-default .cm-hr {color: #999;}
|
||||||
|
.cm-s-default .cm-link {color: #00c;}
|
||||||
|
|
||||||
|
.cm-s-default .cm-error {color: #f00;}
|
||||||
|
.cm-invalidchar {color: #f00;}
|
||||||
|
|
||||||
|
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||||
|
|
||||||
|
/* Default styles for common addons */
|
||||||
|
|
||||||
|
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
|
||||||
|
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||||
|
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||||
|
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||||
|
|
||||||
|
/* STOP */
|
||||||
|
|
||||||
|
/* The rest of this file contains styles related to the mechanics of
|
||||||
|
the editor. You probably shouldn't touch them. */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scroll {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overflow: scroll !important; /* Things will break if this is overridden */
|
||||||
|
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||||
|
/* See overflow: hidden in .CodeMirror */
|
||||||
|
margin-bottom: -30px; margin-right: -30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
height: 100%;
|
||||||
|
outline: none; /* Prevent dragging from highlighting the element */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.CodeMirror-sizer {
|
||||||
|
position: relative;
|
||||||
|
border-right: 30px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||||
|
before actual scrolling happens, thus preventing shaking and
|
||||||
|
flickering artifacts. */
|
||||||
|
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-vscrollbar {
|
||||||
|
right: 0; top: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-hscrollbar {
|
||||||
|
bottom: 0; left: 0;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-scrollbar-filler {
|
||||||
|
right: 0; bottom: 0;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-filler {
|
||||||
|
left: 0; bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
position: absolute; left: 0; top: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
white-space: normal;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-bottom: -30px;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-elt {
|
||||||
|
position: absolute;
|
||||||
|
cursor: default;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
|
||||||
|
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
cursor: text;
|
||||||
|
min-height: 1px; /* prevents collapsing before first draw */
|
||||||
|
}
|
||||||
|
.CodeMirror pre {
|
||||||
|
/* Reset some styles that the rest of the page might have set */
|
||||||
|
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||||
|
border-width: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-font-variant-ligatures: contextual;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
|
}
|
||||||
|
.CodeMirror-wrap pre {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linebackground {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-widget {}
|
||||||
|
|
||||||
|
.CodeMirror-rtl pre { direction: rtl; }
|
||||||
|
|
||||||
|
.CodeMirror-code {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force content-box sizing for the elements where we expect it */
|
||||||
|
.CodeMirror-scroll,
|
||||||
|
.CodeMirror-sizer,
|
||||||
|
.CodeMirror-gutter,
|
||||||
|
.CodeMirror-gutters,
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-measure {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-measure pre { position: static; }
|
||||||
|
|
||||||
|
div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
div.CodeMirror-dragcursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-focused div.CodeMirror-cursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-selected { background: #d9d9d9; }
|
||||||
|
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||||
|
.CodeMirror-crosshair { cursor: crosshair; }
|
||||||
|
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
|
||||||
|
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||||
|
|
||||||
|
.cm-searching {
|
||||||
|
background: #ffa;
|
||||||
|
background: rgba(255, 255, 0, .4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Used to force a border model for a node */
|
||||||
|
.cm-force-border { padding-right: .1px; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Hide the cursor when printing */
|
||||||
|
.CodeMirror div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* See issue #2901 */
|
||||||
|
.cm-tab-wrap-hack:after { content: ''; }
|
||||||
|
|
||||||
|
/* Help users use markselection to safely style text background */
|
||||||
|
span.CodeMirror-selectedtext { background: none; }
|
||||||
48
server/public/css/style.css
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
html {
|
||||||
|
font-family:"San Francisco","Roboto","Arial",sans-serif;
|
||||||
|
-webkit-text-size-adjust:100%;
|
||||||
|
background: #f2f2f2;
|
||||||
|
color: #6f6f6f;
|
||||||
|
}
|
||||||
|
body, html{
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a{color: inherit; text-decoration: none;}
|
||||||
|
|
||||||
|
.scroll-y{
|
||||||
|
overflow-y: scroll!important;
|
||||||
|
overflow-x: hidden!important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.scroll-x{
|
||||||
|
overflow-x: scroll!important;
|
||||||
|
overflow-y: hidden!important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
select{-moz-appearance: none;}
|
||||||
|
select:-moz-focusring {
|
||||||
|
color: inherit;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
select::-ms-expand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.drag-drop{
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.drag-drop.dragging > div{
|
||||||
|
background: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
body {overflow: hidden;}
|
||||||
|
body, body > div, body > div > div, body > div > div > div{ height: 100%;}
|
||||||
1309
server/public/css/video-js.css
Normal file
280
server/public/css/videojs-sublime-skin.css
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
.video-js.my-skin .vjs-menu-button-inline.vjs-slider-active,.video-js.my-skin .vjs-menu-button-inline:focus,.video-js.my-skin .vjs-menu-button-inline:hover,.video-js.my-skin.vjs-no-flex .vjs-menu-button-inline {
|
||||||
|
width: 10em
|
||||||
|
}
|
||||||
|
.video-js.my-skin .vjs-volume-menu-button{display: none;}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-controls-disabled .vjs-big-play-button {
|
||||||
|
display: none!important
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-control {
|
||||||
|
width: 3em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-menu-button-inline:before {
|
||||||
|
width: 1.5em
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-menu-button-inline .vjs-menu {
|
||||||
|
left: 3em
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-paused.vjs-has-started.video-js.my-skin .vjs-big-play-button,.video-js.my-skin.vjs-ended .vjs-big-play-button,.video-js.my-skin.vjs-paused .vjs-big-play-button {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-load-progress div,.vjs-seeking .vjs-big-play-button,.vjs-waiting .vjs-big-play-button {
|
||||||
|
display: none!important
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-mouse-display:after,.video-js.my-skin .vjs-play-progress:after {
|
||||||
|
padding: 0 .4em .3em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.vjs-ended .vjs-loading-spinner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.vjs-ended .vjs-big-play-button {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin *,.video-js.my-skin:after,.video-js.my-skin:before {
|
||||||
|
box-sizing: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
line-height: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.vjs-fullscreen,.video-js.my-skin.vjs-fullscreen .vjs-tech {
|
||||||
|
width: 100%!important;
|
||||||
|
height: 100%!important
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin {
|
||||||
|
font-size: 14px;
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-control {
|
||||||
|
color: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-menu-button-inline:hover,.video-js.my-skin.vjs-no-flex .vjs-menu-button-inline {
|
||||||
|
width: 8.35em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-volume-menu-button.vjs-volume-menu-button-horizontal:hover .vjs-menu .vjs-menu-content {
|
||||||
|
height: 3em;
|
||||||
|
width: 6.35em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-control:focus:before,.video-js.my-skin .vjs-control:hover:before {
|
||||||
|
text-shadow: 0 0 5px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-spacer,.video-js.my-skin .vjs-time-control {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -moz-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-flex: 1 1 auto;
|
||||||
|
-moz-box-flex: 1 1 auto;
|
||||||
|
-webkit-flex: 1 1 auto;
|
||||||
|
-ms-flex: 1 1 auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-time-control {
|
||||||
|
-webkit-box-flex: 0 1 auto;
|
||||||
|
-moz-box-flex: 0 1 auto;
|
||||||
|
-webkit-flex: 0 1 auto;
|
||||||
|
-ms-flex: 0 1 auto;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
width: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-time-control.vjs-time-divider {
|
||||||
|
width: 14px
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-time-control.vjs-time-divider div {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-time-control.vjs-current-time {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-time-control .vjs-current-time-display,.video-js.my-skin .vjs-time-control .vjs-duration-display {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-time-control .vjs-current-time-display {
|
||||||
|
text-align: right
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-time-control .vjs-duration-display {
|
||||||
|
text-align: left
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-play-progress:before,.video-js.my-skin .vjs-progress-control .vjs-play-progress:before,.video-js.my-skin .vjs-remaining-time,.video-js.my-skin .vjs-volume-level:after,.video-js.my-skin .vjs-volume-level:before,.video-js.my-skin.vjs-live .vjs-time-control.vjs-current-time,.video-js.my-skin.vjs-live .vjs-time-control.vjs-duration,.video-js.my-skin.vjs-live .vjs-time-control.vjs-time-divider,.video-js.my-skin.vjs-no-flex .vjs-time-control.vjs-remaining-time {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.vjs-no-flex .vjs-time-control {
|
||||||
|
display: table-cell;
|
||||||
|
width: 4em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-progress-control {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.3em;
|
||||||
|
top: -0.3em;
|
||||||
|
-webkit-transition: all .1s ease 0s;
|
||||||
|
-moz-transition: all .1s ease 0s;
|
||||||
|
-ms-transition: all .1s ease 0s;
|
||||||
|
-o-transition: all .1s ease 0s;
|
||||||
|
transition: all .1s ease 0s
|
||||||
|
}
|
||||||
|
.video-js.my-skin .vjs-progress-control:hover {
|
||||||
|
height: 0.8em;
|
||||||
|
top: -0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-progress-control .vjs-load-progress,.video-js.my-skin .vjs-progress-control .vjs-play-progress,.video-js.my-skin .vjs-progress-control .vjs-progress-holder {
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-progress-control .vjs-progress-holder {
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-control-bar {
|
||||||
|
-webkit-transition: -webkit-transform .1s ease 0s;
|
||||||
|
-moz-transition: -moz-transform .1s ease 0s;
|
||||||
|
-ms-transition: -ms-transform .1s ease 0s;
|
||||||
|
-o-transition: -o-transform .1s ease 0s;
|
||||||
|
transition: transform .1s ease 0s
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.not-hover.vjs-has-started.vjs-paused.vjs-user-active .vjs-control-bar,.video-js.my-skin.not-hover.vjs-has-started.vjs-paused.vjs-user-inactive .vjs-control-bar,.video-js.my-skin.not-hover.vjs-has-started.vjs-playing.vjs-user-active .vjs-control-bar,.video-js.my-skin.not-hover.vjs-has-started.vjs-playing.vjs-user-inactive .vjs-control-bar,.video-js.my-skin.vjs-has-started.vjs-playing.vjs-user-inactive .vjs-control-bar {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
-webkit-transform: translateY(3em);
|
||||||
|
-moz-transform: translateY(3em);
|
||||||
|
-ms-transform: translateY(3em);
|
||||||
|
-o-transform: translateY(3em);
|
||||||
|
transform: translateY(3em);
|
||||||
|
-webkit-transition: -webkit-transform 1s ease 0s;
|
||||||
|
-moz-transition: -moz-transform 1s ease 0s;
|
||||||
|
-ms-transition: -ms-transform 1s ease 0s;
|
||||||
|
-o-transition: -o-transform 1s ease 0s;
|
||||||
|
transition: transform 1s ease 0s
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.not-hover.vjs-has-started.vjs-paused.vjs-user-active .vjs-progress-control,.video-js.my-skin.not-hover.vjs-has-started.vjs-paused.vjs-user-inactive .vjs-progress-control,.video-js.my-skin.not-hover.vjs-has-started.vjs-playing.vjs-user-active .vjs-progress-control,.video-js.my-skin.not-hover.vjs-has-started.vjs-playing.vjs-user-inactive .vjs-progress-control,.video-js.my-skin.vjs-has-started.vjs-playing.vjs-user-inactive .vjs-progress-control {
|
||||||
|
height: .25em;
|
||||||
|
top: -.25em;
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-transition: height 1s,top 1s;
|
||||||
|
-moz-transition: height 1s,top 1s;
|
||||||
|
-ms-transition: height 1s,top 1s;
|
||||||
|
-o-transition: height 1s,top 1s;
|
||||||
|
transition: height 1s,top 1s
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.not-hover.vjs-has-started.vjs-paused.vjs-user-active.vjs-fullscreen .vjs-progress-control,.video-js.my-skin.not-hover.vjs-has-started.vjs-paused.vjs-user-inactive.vjs-fullscreen .vjs-progress-control,.video-js.my-skin.not-hover.vjs-has-started.vjs-playing.vjs-user-active.vjs-fullscreen .vjs-progress-control,.video-js.my-skin.not-hover.vjs-has-started.vjs-playing.vjs-user-inactive.vjs-fullscreen .vjs-progress-control,.video-js.my-skin.vjs-has-started.vjs-playing.vjs-user-inactive.vjs-fullscreen .vjs-progress-control {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 1s ease 1s;
|
||||||
|
-moz-transition: opacity 1s ease 1s;
|
||||||
|
-ms-transition: opacity 1s ease 1s;
|
||||||
|
-o-transition: opacity 1s ease 1s;
|
||||||
|
transition: opacity 1s ease 1s
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin.vjs-live .vjs-live-control {
|
||||||
|
margin-left: 1em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-big-play-button {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -1em;
|
||||||
|
margin-top: -1em;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
line-height: 2em;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 3.5em;
|
||||||
|
background-color: rgba(0,0,0,.45);
|
||||||
|
color: #fff;
|
||||||
|
-webkit-transition: border-color .4s,outline .4s,background-color .4s;
|
||||||
|
-moz-transition: border-color .4s,outline .4s,background-color .4s;
|
||||||
|
-ms-transition: border-color .4s,outline .4s,background-color .4s;
|
||||||
|
-o-transition: border-color .4s,outline .4s,background-color .4s;
|
||||||
|
transition: border-color .4s,outline .4s,background-color .4s
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-menu-button-popup .vjs-menu {
|
||||||
|
left: -3em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-menu-button-popup .vjs-menu .vjs-menu-content {
|
||||||
|
background-color: transparent;
|
||||||
|
width: 12em;
|
||||||
|
left: -1.5em;
|
||||||
|
padding-bottom: .5em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-menu-button-popup .vjs-menu .vjs-menu-item,.video-js.my-skin .vjs-menu-button-popup .vjs-menu .vjs-menu-title {
|
||||||
|
background-color: #151b17;
|
||||||
|
margin: .3em 0;
|
||||||
|
padding: .5em;
|
||||||
|
border-radius: .3em
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-menu-button-popup .vjs-menu .vjs-menu-item.vjs-selected {
|
||||||
|
background-color: #2483d5
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-big-play-button {
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
font-size: 4em;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 1.3em !important;
|
||||||
|
line-height: 1.3em !important;
|
||||||
|
margin-top: -0.65em !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin:hover .vjs-big-play-button,.video-js.my-skin .vjs-big-play-button:focus,.video-js.my-skin .vjs-big-play-button:active {
|
||||||
|
background-color: rgba(255,255,255,0.23)
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-loading-spinner {
|
||||||
|
border-color: rgba(255,255,255,0.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-control-bar2 {
|
||||||
|
background-color: #fcfcfc
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-control-bar {
|
||||||
|
background-color: rgba(252,252,252,0.19) !important;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.my-skin .vjs-play-progress,.video-js.my-skin .vjs-volume-level {
|
||||||
|
background-color: #cccccc
|
||||||
|
}
|
||||||
6
server/public/img/bucket.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 241.75 241.75" enable-background="new 0 0 241.75 241.75" width="512px" height="512px">
|
||||||
|
<g>
|
||||||
|
<path d="m211.875,124.513v-33.513c0-50.178-40.822-91-91-91s-91,40.822-91,91v33.513c-4,0.134-8,3.765-8,8.237v0.5c0,4.557 4.026,8.5 8.583,8.5h13.341l10.275,90.064c0.624,5.465 5.634,9.936 11.134,9.936h112c5.5,0 10.481-4.475 11.067-9.942l9.671-90.058h14.012c4.557,0 7.917-3.943 7.917-8.5v-0.5c0-4.473-3-8.104-8-8.237zm-91-107.513c40.804,0 74,33.196 74,74v1.75h-148v-1.75c0-40.804 33.196-74 74-74z" fill="#6F6F6F"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 676 B |
43
server/public/img/delete.svg
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 482.428 482.429" style="enable-background:new 0 0 482.428 482.429;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M381.163,57.799h-75.094C302.323,25.316,274.686,0,241.214,0c-33.471,0-61.104,25.315-64.85,57.799h-75.098 c-30.39,0-55.111,24.728-55.111,55.117v2.828c0,23.223,14.46,43.1,34.83,51.199v260.369c0,30.39,24.724,55.117,55.112,55.117 h210.236c30.389,0,55.111-24.729,55.111-55.117V166.944c20.369-8.1,34.83-27.977,34.83-51.199v-2.828 C436.274,82.527,411.551,57.799,381.163,57.799z M241.214,26.139c19.037,0,34.927,13.645,38.443,31.66h-76.879 C206.293,39.783,222.184,26.139,241.214,26.139z M375.305,427.312c0,15.978-13,28.979-28.973,28.979H136.096 c-15.973,0-28.973-13.002-28.973-28.979V170.861h268.182V427.312z M410.135,115.744c0,15.978-13,28.979-28.973,28.979H101.266 c-15.973,0-28.973-13.001-28.973-28.979v-2.828c0-15.978,13-28.979,28.973-28.979h279.897c15.973,0,28.973,13.001,28.973,28.979 V115.744z" fill="#6F6F6F"/>
|
||||||
|
<path d="M171.144,422.863c7.218,0,13.069-5.853,13.069-13.068V262.641c0-7.216-5.852-13.07-13.069-13.07 c-7.217,0-13.069,5.854-13.069,13.07v147.154C158.074,417.012,163.926,422.863,171.144,422.863z" fill="#6F6F6F"/>
|
||||||
|
<path d="M241.214,422.863c7.218,0,13.07-5.853,13.07-13.068V262.641c0-7.216-5.854-13.07-13.07-13.07 c-7.217,0-13.069,5.854-13.069,13.07v147.154C228.145,417.012,233.996,422.863,241.214,422.863z" fill="#6F6F6F"/>
|
||||||
|
<path d="M311.284,422.863c7.217,0,13.068-5.853,13.068-13.068V262.641c0-7.216-5.852-13.07-13.068-13.07 c-7.219,0-13.07,5.854-13.07,13.07v147.154C298.213,417.012,304.067,422.863,311.284,422.863z" fill="#6F6F6F"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
10
server/public/img/download.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512px" version="1.1" height="512px" viewBox="0 0 64 64" enable-background="new 0 0 64 64">
|
||||||
|
<g>
|
||||||
|
<g fill="#1D1D1B">
|
||||||
|
<path d="M64,41.733c0-1.345-1.089-2.435-2.435-2.435c-1.347,0-2.436,1.09-2.436,2.435v14.115H4.946V41.733 c0-1.345-1.089-2.435-2.437-2.435c-1.346,0-2.435,1.09-2.435,2.435v15.535c0,0.18,0.067,0.338,0.103,0.507 c-0.035,0.169-0.103,0.328-0.103,0.507c0,1.345,1.089,2.436,2.435,2.436h59.056c1.346,0,2.435-1.091,2.435-2.436 c0-0.179-0.064-0.338-0.102-0.507C63.936,57.606,64,57.448,64,57.269V41.733z" fill="#6F6F6F"/>
|
||||||
|
<path d="m32.19,2.967c-1.346,0-2.437,1.091-2.437,2.436v35.576l-10.605-10.607c-0.951-0.951-2.492-0.951-3.443,0s-0.951,2.493 0,3.444l14.764,14.764c0.475,0.476 1.099,0.713 1.722,0.713 0.622,0 1.245-0.238 1.722-0.713l14.764-14.764c0.951-0.951 0.951-2.493 0-3.444-0.952-0.951-2.493-0.951-3.443,0l-10.609,10.607v-35.576c0-1.345-1.089-2.436-2.435-2.436z" fill="#6F6F6F"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
server/public/img/dropbox.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
9
server/public/img/edit.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 129 129" enable-background="new 0 0 129 129" width="512px" height="512px">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="m119.2,114.3h-109.4c-2.3,0-4.1,1.9-4.1,4.1s1.9,4.1 4.1,4.1h109.5c2.3,0 4.1-1.9 4.1-4.1s-1.9-4.1-4.2-4.1z" fill="#6F6F6F"/>
|
||||||
|
<path d="m5.7,78l-.1,19.5c0,1.1 0.4,2.2 1.2,3 0.8,0.8 1.8,1.2 2.9,1.2l19.4-.1c1.1,0 2.1-0.4 2.9-1.2l67-67c1.6-1.6 1.6-4.2 0-5.9l-19.2-19.4c-1.6-1.6-4.2-1.6-5.9-1.77636e-15l-13.4,13.5-53.6,53.5c-0.7,0.8-1.2,1.8-1.2,2.9zm71.2-61.1l13.5,13.5-7.6,7.6-13.5-13.5 7.6-7.6zm-62.9,62.9l49.4-49.4 13.5,13.5-49.4,49.3-13.6,.1 .1-13.5z" fill="#6F6F6F"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 747 B |
37
server/public/img/error.svg
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 51.976 51.976" style="enable-background:new 0 0 51.976 51.976;" xml:space="preserve" width="512px" height="512px">
|
||||||
|
<g>
|
||||||
|
<path d="M44.373,7.603c-10.137-10.137-26.632-10.138-36.77,0c-10.138,10.138-10.137,26.632,0,36.77s26.632,10.138,36.77,0 C54.51,34.235,54.51,17.74,44.373,7.603z M36.241,36.241c-0.781,0.781-2.047,0.781-2.828,0l-7.425-7.425l-7.778,7.778 c-0.781,0.781-2.047,0.781-2.828,0c-0.781-0.781-0.781-2.047,0-2.828l7.778-7.778l-7.425-7.425c-0.781-0.781-0.781-2.048,0-2.828 c0.781-0.781,2.047-0.781,2.828,0l7.425,7.425l7.071-7.071c0.781-0.781,2.047-0.781,2.828,0c0.781,0.781,0.781,2.047,0,2.828 l-7.071,7.071l7.425,7.425C37.022,34.194,37.022,35.46,36.241,36.241z" fill="#6f6f6f"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
4
server/public/img/file.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<path style="color:#000000;block-progression:tb;text-transform:none;text-indent:0" d="m2.3501 1.0014c-0.19751 0.0382-0.35351 0.23331-0.35001 0.43742v13.123c0.000005 0.22905 0.20523 0.43745 0.43079 0.43746l11.139 0.001c0.22556-0.000006 0.43078-0.20841 0.43079-0.43746v-10.143c-0.004-0.06684-0.022-0.13284-0.054-0.19135l-3.3121-3.1989c-0.043-0.0164-0.088-0.0256-0.134-0.0274l-8.0699-0.001c-0.02684-0.0026-0.05393-0.0026-0.08077 0z" fill="#aaaaaa"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 730 B |
6
server/public/img/folder copy.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<g fill-rule="evenodd" transform="matrix(.86667 0 0 .86667 -172.05 -864.43)" fill="#969696">
|
||||||
|
<path d="m200.2 999.72c-0.28913 0-0.53125 0.2421-0.53125 0.53117v12.784c0 0.2985 0.23264 0.5312 0.53125 0.5312h15.091c0.2986 0 0.53124-0.2327 0.53124-0.5312l0.0004-10.474c0-0.2889-0.24211-0.5338-0.53124-0.5338l-7.5457 0.0005-2.3076-2.3078z" fill-rule="evenodd" fill="#6f6f6f"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
6
server/public/img/folder.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<g fill-rule="evenodd" transform="matrix(.86667 0 0 .86667 -172.05 -864.43)" fill="#969696">
|
||||||
|
<path d="m200.2 999.72c-0.28913 0-0.53125 0.2421-0.53125 0.53117v12.784c0 0.2985 0.23264 0.5312 0.53125 0.5312h15.091c0.2986 0 0.53124-0.2327 0.53124-0.5312l0.0004-10.474c0-0.2889-0.24211-0.5338-0.53124-0.5338l-7.5457 0.0005-2.3076-2.3078z" fill-rule="evenodd" fill="#6f6f6f"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
BIN
server/public/img/google-drive.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
6
server/public/img/link.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<g fill-rule="evenodd" transform="matrix(.86667 0 0 .86667 -172.05 -864.43)" fill="#969696">
|
||||||
|
<path d="m200.2 999.72c-0.28913 0-0.53125 0.2421-0.53125 0.53117v12.784c0 0.2985 0.23264 0.5312 0.53125 0.5312h15.091c0.2986 0 0.53124-0.2327 0.53124-0.5312l0.0004-10.474c0-0.2889-0.24211-0.5338-0.53124-0.5338l-7.5457 0.0005-2.3076-2.3078z" fill-rule="evenodd" fill="#6f6f6f"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
1
server/public/img/loader.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><svg width='120px' height='120px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring-alt"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><circle cx="50" cy="50" r="40" stroke="none" fill="none" stroke-width="10" stroke-linecap="round"></circle><circle cx="50" cy="50" r="40" stroke="#6f6f6f" fill="none" stroke-width="6" stroke-linecap="round"><animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite" from="0" to="502"></animate><animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate></circle></svg>
|
||||||
|
After Width: | Height: | Size: 706 B |
1
server/public/img/loader_white.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><svg width='120px' height='120px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring-alt"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><circle cx="50" cy="50" r="40" stroke="none" fill="none" stroke-width="10" stroke-linecap="round"></circle><circle cx="50" cy="50" r="40" stroke="#ffffff" fill="none" stroke-width="6" stroke-linecap="round"><animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite" from="0" to="502"></animate><animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate></circle></svg>
|
||||||
|
After Width: | Height: | Size: 707 B |
BIN
server/public/img/logo.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
server/public/img/logo_large.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
38
server/public/img/pause.svg
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 47.607 47.607" style="enable-background:new 0 0 47.607 47.607;" xml:space="preserve" width="512px" height="512px">
|
||||||
|
<g>
|
||||||
|
<path d="M17.991,40.976c0,3.662-2.969,6.631-6.631,6.631l0,0c-3.662,0-6.631-2.969-6.631-6.631V6.631C4.729,2.969,7.698,0,11.36,0 l0,0c3.662,0,6.631,2.969,6.631,6.631V40.976z" fill="#6F6F6F"/>
|
||||||
|
<path d="M42.877,40.976c0,3.662-2.969,6.631-6.631,6.631l0,0c-3.662,0-6.631-2.969-6.631-6.631V6.631 C29.616,2.969,32.585,0,36.246,0l0,0c3.662,0,6.631,2.969,6.631,6.631V40.976z" fill="#6F6F6F"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 932 B |
37
server/public/img/play.svg
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 232.153 232.153" style="enable-background:new 0 0 232.153 232.153;" xml:space="preserve" width="512px" height="512px">
|
||||||
|
<g id="Play">
|
||||||
|
<path style="fill-rule:evenodd;clip-rule:evenodd;" d="M203.791,99.628L49.307,2.294c-4.567-2.719-10.238-2.266-14.521-2.266 c-17.132,0-17.056,13.227-17.056,16.578v198.94c0,2.833-0.075,16.579,17.056,16.579c4.283,0,9.955,0.451,14.521-2.267 l154.483-97.333c12.68-7.545,10.489-16.449,10.489-16.449S216.471,107.172,203.791,99.628z" fill="#6F6F6F"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 904 B |
39
server/public/img/power.svg
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 489.888 489.888" style="enable-background:new 0 0 489.888 489.888;" xml:space="preserve" width="512px" height="512px">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M25.383,290.5c-7.2-77.5,25.9-147.7,80.8-192.3c21.4-17.4,53.4-2.5,53.4,25l0,0c0,10.1-4.8,19.4-12.6,25.7 c-38.9,31.7-62.3,81.7-56.6,136.9c7.4,71.9,65,130.1,136.8,138.1c93.7,10.5,173.3-62.9,173.3-154.5c0-48.6-22.5-92.1-57.6-120.6 c-7.8-6.3-12.5-15.6-12.5-25.6l0,0c0-27.2,31.5-42.6,52.7-25.6c50.2,40.5,82.4,102.4,82.4,171.8c0,126.9-107.8,229.2-236.7,219.9 C122.183,481.8,35.283,396.9,25.383,290.5z M244.883,0c-18,0-32.5,14.6-32.5,32.5v149.7c0,18,14.6,32.5,32.5,32.5 s32.5-14.6,32.5-32.5V32.5C277.383,14.6,262.883,0,244.883,0z" fill="#6f6f6f"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
38
server/public/img/save.svg
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 438.533 438.533" style="enable-background:new 0 0 438.533 438.533;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M432.823,121.049c-3.806-9.132-8.377-16.367-13.709-21.695l-79.941-79.942c-5.325-5.325-12.56-9.895-21.696-13.704 C308.346,1.903,299.969,0,292.357,0H27.409C19.798,0,13.325,2.663,7.995,7.993c-5.33,5.327-7.992,11.799-7.992,19.414v383.719 c0,7.617,2.662,14.089,7.992,19.417c5.33,5.325,11.803,7.991,19.414,7.991h383.718c7.618,0,14.089-2.666,19.417-7.991 c5.325-5.328,7.987-11.8,7.987-19.417V146.178C438.531,138.562,436.629,130.188,432.823,121.049z M182.725,45.677 c0-2.474,0.905-4.611,2.714-6.423c1.807-1.804,3.949-2.708,6.423-2.708h54.819c2.468,0,4.609,0.902,6.417,2.708 c1.813,1.812,2.717,3.949,2.717,6.423v91.362c0,2.478-0.91,4.618-2.717,6.427c-1.808,1.803-3.949,2.708-6.417,2.708h-54.819 c-2.474,0-4.617-0.902-6.423-2.708c-1.809-1.812-2.714-3.949-2.714-6.427V45.677z M328.906,401.991H109.633V292.355h219.273 V401.991z M402,401.991h-36.552h-0.007V283.218c0-7.617-2.663-14.085-7.991-19.417c-5.328-5.328-11.8-7.994-19.41-7.994H100.498 c-7.614,0-14.087,2.666-19.417,7.994c-5.327,5.328-7.992,11.8-7.992,19.417v118.773H36.544V36.542h36.544v118.771 c0,7.615,2.662,14.084,7.992,19.414c5.33,5.327,11.803,7.993,19.417,7.993h164.456c7.61,0,14.089-2.666,19.41-7.993 c5.325-5.327,7.994-11.799,7.994-19.414V36.542c2.854,0,6.563,0.95,11.136,2.853c4.572,1.902,7.806,3.805,9.709,5.708l80.232,80.23 c1.902,1.903,3.806,5.19,5.708,9.851c1.909,4.665,2.857,8.33,2.857,10.994V401.991z" fill="#FFFFFF"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2 KiB |
1
server/public/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!DOCTYPE html> <html> <head> <meta charset=utf-8> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name=viewport> <meta content=yes name=apple-mobile-web-app-capable> <meta content=Nuage name=apple-mobile-web-app-title> <meta content=black-translucent name=apple-mobile-web-app-status-bar-style> <link rel=apple-touch-icon href=/img/logo.png> <title>Nuage</title> <meta name=description content="browse your files in the cloud"> <link rel=stylesheet href=/css/style.css> </head> <body> <div id=main></div> <link rel=stylesheet href=/css/codemirror.css> <link rel=stylesheet href=/css/videojs-sublime-skin.css> <link rel=stylesheet href=/css/video-js.css> <script type="text/javascript" src="/bundle.js"></script></body> </html>
|
||||||
26
server/utils/crypto.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
var crypto = require('crypto'),
|
||||||
|
algorithm = 'aes-256-ctr',
|
||||||
|
password = process.env.SECRET_KEY || '123';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
encrypt: function(obj){
|
||||||
|
obj.date = new Date().getTime();
|
||||||
|
let text = JSON.stringify(obj);
|
||||||
|
var cipher = crypto.createCipher(algorithm,password)
|
||||||
|
var crypted = cipher.update(text,'utf8','hex')
|
||||||
|
crypted += cipher.final('hex');
|
||||||
|
return crypted;
|
||||||
|
},
|
||||||
|
decrypt: function(text){
|
||||||
|
var dec;
|
||||||
|
try{
|
||||||
|
var decipher = crypto.createDecipher(algorithm,password)
|
||||||
|
dec = decipher.update(text,'hex','utf8')
|
||||||
|
dec += decipher.final('utf8');
|
||||||
|
dec = JSON.parse(dec);
|
||||||
|
}catch(err){
|
||||||
|
dec = {};
|
||||||
|
}
|
||||||
|
return dec;
|
||||||
|
}
|
||||||
|
}
|
||||||
217
server/utils/mimetype.js
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
module.exports.getMimeType = function(file){
|
||||||
|
let ext = path.extname(file).replace(/^\./, '').toLowerCase();
|
||||||
|
let mime = db[ext];
|
||||||
|
if(mime){
|
||||||
|
return mime;
|
||||||
|
}else{
|
||||||
|
return 'text/plain';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.opener = function(file){
|
||||||
|
let mime = getMimeType(file);
|
||||||
|
if(mime.split('/')[0] === 'text'){
|
||||||
|
return 'editor';
|
||||||
|
}else if(mime === 'application/pdf'){
|
||||||
|
return 'pdf';
|
||||||
|
}else if(mime.split('/')[0] === 'image'){
|
||||||
|
return 'image';
|
||||||
|
}else if(['application/javascript', 'application/xml'].indexOf(mime) !== -1){
|
||||||
|
return 'editor';
|
||||||
|
}else if(['audio/wav', 'audio/mp3', 'audio/flac'].indexOf(mime) !== -1){
|
||||||
|
return 'audio';
|
||||||
|
}else if(['video/webm', 'video/mp4', 'application/ogg'].indexOf(mime) !== -1){
|
||||||
|
return 'video';
|
||||||
|
}else{
|
||||||
|
return 'download';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const db = {
|
||||||
|
'pdf': 'application/pdf',
|
||||||
|
'csv': 'text/csv',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'bmp': 'image/bmp',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'svg': 'image/svg',
|
||||||
|
'png': 'image/png',
|
||||||
|
'ico': 'image/x-icon',
|
||||||
|
'cab': 'application/vnd.ms-cab-compressed',
|
||||||
|
'mpeg': 'audio/mpeg',
|
||||||
|
'mpg': 'audio/mpeg',
|
||||||
|
'aif': 'audio/x-aiff',
|
||||||
|
'aiff': 'audio/x-aiff',
|
||||||
|
'ra': 'audio/x-pn-realaudio',
|
||||||
|
'ram': 'audio/x-pn-realaudio',
|
||||||
|
'wav': 'audio/wave',
|
||||||
|
'mp3': 'audio/mp3',
|
||||||
|
'flac': 'audio/flac',
|
||||||
|
'wma': 'audio/x-ms-wma',
|
||||||
|
'wmv': 'video/x-ms-wmv',
|
||||||
|
'webm': 'video/webm',
|
||||||
|
'mp4': 'video/mp4',
|
||||||
|
'mov': 'video/quicktime',
|
||||||
|
'avi': 'video/x-msvideo',
|
||||||
|
'ogg': 'application/ogg',
|
||||||
|
'ogv': 'application/ogg',
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'xml': 'application/xml',
|
||||||
|
'deb': 'application/vnd.debian.binary-package',
|
||||||
|
'dpkg': 'application/dpkg-www-installer',
|
||||||
|
'rpm': 'application/x-rpm',
|
||||||
|
'apk': 'application/vnd.android.package-archive',
|
||||||
|
'exe': 'application/x-msdownload',
|
||||||
|
'msi': 'application/x-msdownload',
|
||||||
|
'dmg': 'application/x-apple-diskimage',
|
||||||
|
'pkg': 'application/x-newton-compatible-pkg',
|
||||||
|
'tar': 'application/x-tar',
|
||||||
|
'zip': 'application/x-zip',
|
||||||
|
'gz': 'application/x-gzip',
|
||||||
|
'bz2': 'application/x-bz2',
|
||||||
|
'rar': 'application/x-rar-compressed',
|
||||||
|
'so': 'application/octet-stream',
|
||||||
|
'eps': 'application/postscript',
|
||||||
|
'ps': 'application/postscript',
|
||||||
|
'midi': 'application/x-midi',
|
||||||
|
'odg': 'application/vnd.oasis.opendocument.graphics',
|
||||||
|
'odp': 'application/vnd.oasis.opendocument.presentation',
|
||||||
|
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
||||||
|
'odt': 'application/vnd.oasis.opendocument.text',
|
||||||
|
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'doc': 'application/msword',
|
||||||
|
'pps': 'application/vnd.ms-powerpoint',
|
||||||
|
'ppt': 'application/vnd.ms-powerpoint',
|
||||||
|
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'rtf': 'application/rtf',
|
||||||
|
'swf': 'application/x-shockwave-flash',
|
||||||
|
'vrml': 'application/x-vrml',
|
||||||
|
'wrl': 'x-world/x-vrml',
|
||||||
|
'xls': 'application/vnd.ms-excel',
|
||||||
|
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'ds_store': 'application/octet-stream',
|
||||||
|
// FROM nginx
|
||||||
|
"html": "text/html",
|
||||||
|
"shtml": "text/html",
|
||||||
|
"htm": "text/html",
|
||||||
|
"css": "text/css",
|
||||||
|
"xml": "text/xml",
|
||||||
|
"atom": "application/atom+xml",
|
||||||
|
"rss": "application/rss+xml",
|
||||||
|
"mml": "text/mathml",
|
||||||
|
"txt": "text/plain",
|
||||||
|
"jad": "text/vnd.sun.j2me.app-descriptor",
|
||||||
|
"wml": "text/vnd.wap.wml",
|
||||||
|
"htc": "text/x-component",
|
||||||
|
"tif": "image/tiff",
|
||||||
|
"tiff": "image/tiff",
|
||||||
|
"wbmp": "image/vnd.wap.wbmp",
|
||||||
|
"ico": "image/x-icon",
|
||||||
|
"jng": "image/x-jng",
|
||||||
|
"bmp": "image/x-ms-bmp",
|
||||||
|
"svg": "image/svg+xml",
|
||||||
|
"svgz": "image/svg+xml",
|
||||||
|
"webp": "image/webp",
|
||||||
|
"woff": "application/font-woff",
|
||||||
|
"jar": "application/java-archive",
|
||||||
|
"ear": "application/java-archive",
|
||||||
|
"war": "application/java-archive",
|
||||||
|
"json": "application/json",
|
||||||
|
"hqx": "application/mac-binhex40",
|
||||||
|
"doc": "application/msword",
|
||||||
|
"ai": "application/postscript",
|
||||||
|
"m3u8": "application/vnd.apple.mpegurl",
|
||||||
|
"eot": "application/vnd.ms-fontobject",
|
||||||
|
"wmlc": "application/vnd.wap.wmlc",
|
||||||
|
"kml": "application/vnd.google-earth.kml+xml",
|
||||||
|
"kmz": "application/vnd.google-earth.kmz",
|
||||||
|
"7z": "application/x-7z-compressed",
|
||||||
|
"cco": "application/x-cocoa",
|
||||||
|
"jardiff": "application/x-java-archive-diff",
|
||||||
|
"jnlp": "application/x-java-jnlp-file",
|
||||||
|
"run": "application/x-makeself",
|
||||||
|
"pl": "application/x-perl",
|
||||||
|
"pm": "application/x-perl",
|
||||||
|
"prc": "application/x-pilot",
|
||||||
|
"pdb": "application/x-pilot",
|
||||||
|
"rar": "application/x-rar-compressed",
|
||||||
|
"rpm": "application/x-redhat-package-manager",
|
||||||
|
"sea": "application/x-sea",
|
||||||
|
"swf": "application/x-shockwave-flash",
|
||||||
|
"sit": "application/x-stuffit",
|
||||||
|
"tcl": "application/x-tcl",
|
||||||
|
"tk": "application/x-tcl",
|
||||||
|
"der": "application/x-x509-ca-cert",
|
||||||
|
"crt": "application/x-x509-ca-cert",
|
||||||
|
"pem": "application/x-x509-ca-cert",
|
||||||
|
"xpi": "application/x-xpinstall",
|
||||||
|
"xhtml": "application/xhtml+xml",
|
||||||
|
"xspf": "application/xspf+xml",
|
||||||
|
"zip": "application/zip",
|
||||||
|
"bin": "application/octet-stream",
|
||||||
|
"dll": "application/octet-stream",
|
||||||
|
"exe": "application/octet-stream",
|
||||||
|
"deb": "application/octet-stream",
|
||||||
|
"dmg": "application/octet-stream",
|
||||||
|
"iso": "application/octet-stream",
|
||||||
|
"img": "application/octet-stream",
|
||||||
|
"msi": "application/octet-stream",
|
||||||
|
"msm": "application/octet-stream",
|
||||||
|
"msp": "application/octet-stream",
|
||||||
|
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"mid": "audio/midi",
|
||||||
|
"kar": "audio/midi",
|
||||||
|
"midi": "audio/midi",
|
||||||
|
"ogg": "audio/ogg",
|
||||||
|
"m4a": "audio/x-m4a",
|
||||||
|
"ra": "audio/x-realaudio",
|
||||||
|
"swf": "application/x-shockwave-flash",
|
||||||
|
"sit": "application/x-stuffit",
|
||||||
|
"tcl": "application/x-tcl",
|
||||||
|
"tk": "application/x-tcl",
|
||||||
|
"der": "application/x-x509-ca-cert",
|
||||||
|
"crt": "application/x-x509-ca-cert",
|
||||||
|
"pem": "application/x-x509-ca-cert",
|
||||||
|
"xpi": "application/x-xpinstall",
|
||||||
|
"xhtml": "application/xhtml+xml",
|
||||||
|
"xspf": "application/xspf+xml",
|
||||||
|
"zip": "application/zip",
|
||||||
|
"bin": "application/octet-stream",
|
||||||
|
"dll": "application/octet-stream",
|
||||||
|
"exe": "application/octet-stream",
|
||||||
|
"deb": "application/octet-stream",
|
||||||
|
"dmg": "application/octet-stream",
|
||||||
|
"iso": "application/octet-stream",
|
||||||
|
"img": "application/octet-stream",
|
||||||
|
"msi": "application/octet-stream",
|
||||||
|
"msm": "application/octet-stream",
|
||||||
|
"msp": "application/octet-stream",
|
||||||
|
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"mid": "audio/midi",
|
||||||
|
"kar": "audio/midi",
|
||||||
|
"midi": "audio/midi",
|
||||||
|
"ogg": "audio/ogg",
|
||||||
|
"m4a": "audio/x-m4a",
|
||||||
|
"ra": "audio/x-realaudio",
|
||||||
|
"3gpp": "video/3gpp",
|
||||||
|
"3gp": "video/3gpp",
|
||||||
|
"ts": "video/mp2t",
|
||||||
|
"mp4": "video/mp4",
|
||||||
|
"mpg": "video/mpeg",
|
||||||
|
"mov": "video/quicktime",
|
||||||
|
"webm": "video/webm",
|
||||||
|
"flv": "video/x-flv",
|
||||||
|
"m4v": "video/x-m4v",
|
||||||
|
"mng": "video/x-mng",
|
||||||
|
"asx": "video/x-ms-asf",
|
||||||
|
"asf": "video/x-ms-asf",
|
||||||
|
"wmv": "video/x-ms-wmv",
|
||||||
|
"avi": "video/x-msvideo"
|
||||||
|
}
|
||||||
8
src/client.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// src/app-client.js
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import Router from './router';
|
||||||
|
|
||||||
|
window.onload = () => {
|
||||||
|
ReactDOM.render(<Router/>, document.getElementById('main'));
|
||||||
|
};
|
||||||
1
src/components/api.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import React from 'react';
|
||||||
169
src/components/breadcrumb.js
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { theme } from '../utilities/theme';
|
||||||
|
import { NgIf, Icon } from '../utilities/';
|
||||||
|
import { EventEmitter, EventReceiver } from '../data';
|
||||||
|
|
||||||
|
export class BreadCrumb extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
path: this._formatPath(props.path)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(props){
|
||||||
|
this.setState({path: this._formatPath(props.path)});
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatPath(full_path){
|
||||||
|
let paths = full_path.split("/");
|
||||||
|
if(paths.slice(-1)[0] === ''){
|
||||||
|
paths.pop();
|
||||||
|
}
|
||||||
|
paths = paths.map((path, index) => {
|
||||||
|
let sub_path = paths.slice(0, index+1).join('/'),
|
||||||
|
label = path === ''? 'Nuage' : path;
|
||||||
|
if(index === paths.length - 1){
|
||||||
|
return {full: null, label: label};
|
||||||
|
}else{
|
||||||
|
return {full: sub_path+'/', label: label}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(Element) {
|
||||||
|
const Path = Element? Element : PathElement;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BreadCrumbContainer className={this.props.className}>
|
||||||
|
<Logout />
|
||||||
|
{
|
||||||
|
this.state.path.map((path, index) => {
|
||||||
|
return (
|
||||||
|
<span key={index}>
|
||||||
|
<Path path={path} isLast={this.state.path.length === index + 1} needSaving={this.props.needSaving} />
|
||||||
|
<Separator isLast={this.state.path.length === index + 1} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</BreadCrumbContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BreadCrumb.propTypes = {
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
needSaving: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const BreadCrumbContainer = (props) => {
|
||||||
|
let style1 = {background: 'white', margin: '0 0 0px 0', padding: '6px 0', boxShadow: '0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.2)', zIndex: '1000', position: 'relative'};
|
||||||
|
let style2 = {margin: '0 auto', width: '95%', maxWidth: '800px', padding: '0'};
|
||||||
|
return (
|
||||||
|
<div className={props.className} style={style1}>
|
||||||
|
<ul style={style2}>
|
||||||
|
{props.children}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const Logout = (props) => {
|
||||||
|
let style = {
|
||||||
|
float: 'right',
|
||||||
|
fontSize: '17px',
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 0px 6px 6px',
|
||||||
|
margin: '0 0px'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li style={style}>
|
||||||
|
<Link to="/logout">
|
||||||
|
<Icon style={{height: '20px'}} name="power"/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Saving = (props) => {
|
||||||
|
let style = {
|
||||||
|
display: 'inline',
|
||||||
|
padding: '0 3px'
|
||||||
|
};
|
||||||
|
|
||||||
|
if(props.needSaving){
|
||||||
|
return (
|
||||||
|
<NgIf style={style} cond={props.needSaving === true && props.isLast === true}>
|
||||||
|
*
|
||||||
|
</NgIf>
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Separator = (props) => {
|
||||||
|
return (
|
||||||
|
<NgIf cond={props.isLast === false} style={{display: 'inline', fontFamily: 'monospace', color: '#aaaaaa'}}>
|
||||||
|
>
|
||||||
|
</NgIf>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventEmitter
|
||||||
|
export class PathElementWrapper extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(){
|
||||||
|
if(this.props.isLast === false){
|
||||||
|
this.props.emit('file.select', this.props.path.full, 'directory')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
let style = {
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '17px',
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '5px 3px',
|
||||||
|
margin: '0 4px',
|
||||||
|
fontWeight: this.props.isLast ? '100': ''
|
||||||
|
};
|
||||||
|
if(this.props.highlight === true){
|
||||||
|
style.background = 'rgba(209, 255, 255,0.5)';
|
||||||
|
style.border = '2px solid #38a6a6';
|
||||||
|
style.borderRadius = '2px';
|
||||||
|
style.padding = '3px 20px';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li onClick={this.onClick.bind(this)} style={style}>
|
||||||
|
{this.props.path.label}
|
||||||
|
<Saving isLast={this.props.isLast} needSaving={this.props.needSaving} isSaving={false} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// just a hack to make it play nicely with react-dnd as it refuses to use our custom component if it's not wrap by something it knows ...
|
||||||
|
export class PathElement extends PathElementWrapper {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(highlight = false){
|
||||||
|
return (
|
||||||
|
<div style={{display: 'inline-block'}}>
|
||||||
|
<PathElementWrapper highlight={highlight} {...this.props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/components/connect.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export class Connect extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
CONNECT
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/components/editor.js
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import CodeMirror from 'codemirror/lib/codemirror';
|
||||||
|
import 'codemirror/keymap/emacs.js';
|
||||||
|
import 'codemirror/addon/mode/simple';
|
||||||
|
import 'codemirror/addon/search/searchcursor.js';
|
||||||
|
import 'codemirror/addon/search/search.js';
|
||||||
|
import 'codemirror/addon/edit/matchbrackets.js';
|
||||||
|
import 'codemirror/addon/comment/comment.js';
|
||||||
|
import 'codemirror/addon/dialog/dialog.js';
|
||||||
|
//import '../pages/editpage/javascript';
|
||||||
|
|
||||||
|
CodeMirror.defineSimpleMode("orgmode", {
|
||||||
|
start: [
|
||||||
|
{regex: /(^[\*]+)(\s[TODO|NEXT|DONE|DEFERRED|REJECTED|WAITING]{2,})?(.*)/, token: ['comment', 'qualifier', 'header']}, // headline
|
||||||
|
{regex: /\s*\:?[A-Z_]+\:.*/, token: "qualifier"}, // property drawers
|
||||||
|
{regex: /(\#\+[A-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments
|
||||||
|
{regex: /\[\[[^\[\]]*\]\[[^\[\]]*\]\]/, token: "url"}, // links
|
||||||
|
{regex: /\[[xX\s]?\]/, token: 'qualifier'}, // checkbox
|
||||||
|
{regex: /\#\+BEGIN_[A-Z]*/, token: "comment", next: "env"}, // comments
|
||||||
|
],
|
||||||
|
env: [
|
||||||
|
{regex: /.*?\#\+END_[A-Z]*/, token: "comment", next: "start"},
|
||||||
|
{regex: /.*/, token: "comment"}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
dontIndentStates: ["comment"],
|
||||||
|
lineComment: "//"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export class Editor extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
editor: null,
|
||||||
|
filename: this.props.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(props){
|
||||||
|
if(this.props.content !== props.content){
|
||||||
|
this.state.editor.getDoc().setValue(props.content);
|
||||||
|
}
|
||||||
|
if(this.props.height !== props.height){
|
||||||
|
this.updateHeight(props.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
this.loadMode(this.props.filename)
|
||||||
|
.then(loadCodeMirror.bind(this))
|
||||||
|
|
||||||
|
function loadCodeMirror(mode){
|
||||||
|
//console.log(mode)
|
||||||
|
let editor = CodeMirror(document.getElementById('editor'), {
|
||||||
|
value: this.props.content,
|
||||||
|
lineNumbers: document.body.offsetWidth > 500 ? true : false,
|
||||||
|
mode: mode,
|
||||||
|
lineWrapping: true,
|
||||||
|
keyMap: "emacs"
|
||||||
|
});
|
||||||
|
this.setState({editor: editor});
|
||||||
|
this.updateHeight(this.props.height);
|
||||||
|
|
||||||
|
editor.on('change', (edit) => {
|
||||||
|
if(this.props.onChange){
|
||||||
|
this.props.onChange(edit.getValue());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
CodeMirror.commands.save = () => {
|
||||||
|
let elt = editor.getWrapperElement();
|
||||||
|
elt.style.background = "rgba(0,0,0,0.1)";
|
||||||
|
window.setTimeout(function() { elt.style.background = ""; }, 300);
|
||||||
|
this.props.onSave && this.props.onSave();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
this.state.editor.clearHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHeight(height){
|
||||||
|
if(height){
|
||||||
|
//document.querySelector('.CodeMirror').style.height = height+'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadMode(file){
|
||||||
|
let ext = file.split('.').pop(),
|
||||||
|
mode = null;
|
||||||
|
|
||||||
|
ext = ext.replace(/~$/, ''); // remove emacs mark when a file is opened
|
||||||
|
|
||||||
|
if(ext === 'org' || ext === 'org_archive'){ return Promise.resolve('orgmode'); }
|
||||||
|
else if(ext === 'js' || ext === 'json'){
|
||||||
|
// import('../pages/editpage/index')
|
||||||
|
// .then((m) => {console.log(m);})
|
||||||
|
// .catch((err) => {
|
||||||
|
// console.trace(err)
|
||||||
|
// })
|
||||||
|
// require(["../pages/editpage/javascript"], function(a) {
|
||||||
|
// console.log("DONEEE");
|
||||||
|
// console.log("HEREEE")
|
||||||
|
// }, function(err){
|
||||||
|
// console.log(err)
|
||||||
|
// });
|
||||||
|
|
||||||
|
//
|
||||||
|
// return System.import('../pages/editpage/index')
|
||||||
|
// .then((mode) => {
|
||||||
|
// console.log(mode)
|
||||||
|
// return Promise.resolve('javascript')
|
||||||
|
// })
|
||||||
|
//System.import('codemirror/mode/javascript/javascript')
|
||||||
|
return Promise.resolve('javascript')
|
||||||
|
}
|
||||||
|
// else if(ext === 'sh'){ mode = 'shell'; }
|
||||||
|
// else if(ext === 'py'){ mode = 'python'; }
|
||||||
|
// else if(ext === 'html'){ mode = 'htmlmixed'; }
|
||||||
|
// else if(ext === 'css'){ mode = 'css'; }
|
||||||
|
// else if(ext === 'erl'){ mode = 'erlang'; }
|
||||||
|
// else if(ext === 'go'){mode = 'go'; }
|
||||||
|
// else if(ext === 'markdown' || ext === 'md'){mode = 'markdown'; }
|
||||||
|
// else if(ext === 'pl'){mode = 'perl'; }
|
||||||
|
// else if(ext === 'clj'){ mode = 'clojure'; }
|
||||||
|
// else if(ext === 'php'){ mode = 'php'; }
|
||||||
|
// else if(ext === 'r'){ mode = 'r'; }
|
||||||
|
// else if(ext === 'rb'){ mode = 'ruby'; }
|
||||||
|
// else if(ext === 'less' || ext === 'scss'){ mode = 'sass'; }
|
||||||
|
// else if(ext === 'sql'){ mode = 'sql'; }
|
||||||
|
// else if(ext === 'xml'){ mode = 'xml'; }
|
||||||
|
// else if(ext === 'yml'){
|
||||||
|
// System.import('codemirror/mode/javascript/javascript')
|
||||||
|
// //.then(() => Promise.resolve('javascript'));
|
||||||
|
// }
|
||||||
|
// else if(ext === 'c' || ext === 'cpp' || ext === 'java'){
|
||||||
|
// mode = 'clike';
|
||||||
|
// }
|
||||||
|
else{ return Promise.resolve('orgmode') }
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="editor" style={{height: '100%'}}></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.propTypes = {
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
filename: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onSave: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
// function load(mode){
|
||||||
|
// let url = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.26.0/mode/'+mode+'/'+mode+'.js';
|
||||||
|
// var script = document.createElement('script');
|
||||||
|
// script.type = 'text/javascript';
|
||||||
|
// script.src = url;
|
||||||
|
// document.getElementsByTagName('head')[0].appendChild(script);
|
||||||
|
// return mode;
|
||||||
|
// }
|
||||||
121
src/components/filesystem.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Path from 'path';
|
||||||
|
import { Container, NgIf } from '../utilities';
|
||||||
|
import { NewThing, ExistingThing, FileZone } from '../pages/filespage/';
|
||||||
|
import { DropTarget } from 'react-dnd';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@DropTarget('__NATIVE_FILE__', {}, (connect, monitor) => ({
|
||||||
|
connectDropFile: connect.dropTarget(),
|
||||||
|
fileIsOver: monitor.isOver()
|
||||||
|
}))
|
||||||
|
export class FileSystem extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
creating: null,
|
||||||
|
access_right: this._findAccessRight(props.files),
|
||||||
|
sort: 'type'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_findAccessRight(files){
|
||||||
|
for(let i=0, l=files.length; i< l; i++){
|
||||||
|
let file = files[i];
|
||||||
|
if(file.name === './' && file.type === 'metadata'){
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {can_create_file: true, can_create_directory: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(files, type){
|
||||||
|
if(type === 'name'){
|
||||||
|
return sortByName(files);
|
||||||
|
}else if(type === 'date'){
|
||||||
|
return sortByDate(files);
|
||||||
|
}else{
|
||||||
|
return sortByType(files);
|
||||||
|
}
|
||||||
|
function sortByType(files){
|
||||||
|
return files.sort((fileA, fileB) => {
|
||||||
|
let idA = ['deleting', 'moving'].indexOf(fileA.state),
|
||||||
|
idB = ['deleting', 'moving'].indexOf(fileB.state);
|
||||||
|
|
||||||
|
if(idA !== -1 && idB !== -1){ return 0; }
|
||||||
|
else if(idA !== -1 && idB === -1){ return +1; }
|
||||||
|
else if(idA === -1 && idB !== -1){ return -1; }
|
||||||
|
else{
|
||||||
|
if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) !== -1 ){ return 0; }
|
||||||
|
else if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) === -1){ return -1; }
|
||||||
|
else if(['directory', 'link'].indexOf(fileA.type) === -1 && ['directory', 'link'].indexOf(fileB.type) !== -1){ return +1; }
|
||||||
|
else{ return fileA.name.toLowerCase() > fileB.name.toLowerCase(); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function sortByName(files){
|
||||||
|
return files.sort((fileA, fileB) => {
|
||||||
|
let idA = ['deleting', 'moving'].indexOf(fileA.state),
|
||||||
|
idB = ['deleting', 'moving'].indexOf(fileB.state);
|
||||||
|
|
||||||
|
if(idA !== -1 && idB !== -1){ return 0; }
|
||||||
|
else if(idA !== -1 && idB === -1){ return +1; }
|
||||||
|
else if(idA === -1 && idB !== -1){ return -1; }
|
||||||
|
else{ return fileA.name.toLowerCase() > fileB.name.toLowerCase(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function sortByDate(files){
|
||||||
|
return files.sort((fileA, fileB) => {
|
||||||
|
let idA = ['deleting', 'moving'].indexOf(fileA.state),
|
||||||
|
idB = ['deleting', 'moving'].indexOf(fileB.state);
|
||||||
|
|
||||||
|
if(idA !== -1 && idB !== -1){ return 0; }
|
||||||
|
else if(idA !== -1 && idB === -1){ return +1; }
|
||||||
|
else if(idA === -1 && idB !== -1){ return -1; }
|
||||||
|
else{ return fileB.time - fileA.time; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onComponentPropsUpdate(props){
|
||||||
|
this.setState({access_right: this._findAccessRight(props.files)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// IN NEW THING
|
||||||
|
// onCreating={(value) => this.setState({creating: value})} onCreate={this.onCreate.bind(this)}
|
||||||
|
|
||||||
|
// IN FILEZONE
|
||||||
|
// onUpload={this.onUpload.bind(this)}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.props.connectDropFile(
|
||||||
|
<div style={{height: '100%'}}>
|
||||||
|
<Container style={{height: '100%'}}>
|
||||||
|
<NewThing path={this.props.path} sort={this.state.sort} onSortUpdate={(value) => {this.setState({sort: value})}} accessRight={this.state.access_right}></NewThing>
|
||||||
|
<NgIf cond={this.props.fileIsOver}>
|
||||||
|
<FileZone path={this.props.path} />
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.props.files.length > 0} style={{clear: 'both', paddingBottom: '15px'}}>
|
||||||
|
{
|
||||||
|
this.sort(this.props.files, this.state.sort).map((file, index) => {
|
||||||
|
if(file.type === 'directory' || file.type === 'file' || file.type === 'link' || file.type === 'bucket'){
|
||||||
|
return <ExistingThing key={file.name} file={file} path={this.props.path} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.props.files.length === 0 && !this.state.creating} style={{fontSize: '25px', textAlign: 'center', fontWeight: '100', marginTop: '50px'}}>
|
||||||
|
There is nothing here
|
||||||
|
</NgIf>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSystem.PropTypes = {
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
files: PropTypes.array.isRequired
|
||||||
|
}
|
||||||
4
src/components/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { BreadCrumb } from './breadcrumb';
|
||||||
|
export { Editor } from './editor';
|
||||||
|
export { FileSystem } from './filesystem';
|
||||||
|
export { Connect } from './connect';
|
||||||
107
src/data/api.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { http_get, http_post, http_delete, invalidate } from './tools';
|
||||||
|
import { prepare } from '../utilities/navigate';
|
||||||
|
import Path from 'path';
|
||||||
|
|
||||||
|
function invalidate_ls(path, exact = true){
|
||||||
|
let url = '/api/files/ls?path='.replace(/([^a-zA-Z0-9])/g, '\\$1');
|
||||||
|
let reg = new RegExp(url + prepare(Path.dirname(path)+'.*'));
|
||||||
|
return invalidate(reg);
|
||||||
|
}
|
||||||
|
function invalidate_cat(path, exact = true){
|
||||||
|
let url = '/api/files/cat?path='.replace(/([^a-zA-Z0-9])/g, '\\$1');
|
||||||
|
let reg = new RegExp(url + prepare(path)+ (exact? '' : '.*'));
|
||||||
|
return invalidate(reg);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileSystem{
|
||||||
|
ls(path, cache = 120){
|
||||||
|
let url = '/api/files/ls?path='+prepare(path);
|
||||||
|
invalidate(path)
|
||||||
|
return http_get(url, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
rm(path){
|
||||||
|
let url = '/api/files/rm?path='+prepare(path);
|
||||||
|
invalidate_ls(path), false;
|
||||||
|
invalidate_cat(path, false);
|
||||||
|
return http_get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
mv(from, to){
|
||||||
|
let url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to);
|
||||||
|
invalidate_ls(from);
|
||||||
|
invalidate_ls(to);
|
||||||
|
invalidate_cat(from);
|
||||||
|
return http_get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
cat(path, cache = 60){
|
||||||
|
let url = '/api/files/cat?path='+prepare(path);
|
||||||
|
return http_get(url, cache, 'raw')
|
||||||
|
}
|
||||||
|
url(path){
|
||||||
|
let url = '/api/files/cat?path='+prepare(path);
|
||||||
|
return Promise.resolve(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
save(path, file){
|
||||||
|
invalidate_ls(path);
|
||||||
|
invalidate_cat(path);
|
||||||
|
let url = '/api/files/cat?path='+prepare(path);
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
return http_post(url, formData, 'multipart');
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir(path){
|
||||||
|
let url = '/api/files/mkdir?path='+prepare(path);
|
||||||
|
invalidate_ls(path);
|
||||||
|
return http_get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
touch(path, file){
|
||||||
|
invalidate_ls(path);
|
||||||
|
if(file){
|
||||||
|
let url = '/api/files/cat?path='+prepare(path);
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
return http_post(url, formData, 'multipart');
|
||||||
|
}else{
|
||||||
|
let url = '/api/files/touch?path='+prepare(path);
|
||||||
|
return http_get(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionManager{
|
||||||
|
isLogged(){
|
||||||
|
let url = '/api/session'
|
||||||
|
return http_get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
url(type){
|
||||||
|
if(type === 'dropbox'){
|
||||||
|
let url = '/api/session/auth/dropbox';
|
||||||
|
return http_get(url);
|
||||||
|
}else if(type === 'gdrive'){
|
||||||
|
let url = '/api/session/auth/gdrive';
|
||||||
|
return http_get(url);
|
||||||
|
}else{
|
||||||
|
return Promise.error({message: 'not authorization backend for: '+type, code: 'UNKNOWN_PROVIDER'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate(params){
|
||||||
|
let url = '/api/session';
|
||||||
|
return http_post(url, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(){
|
||||||
|
let url = '/api/session';
|
||||||
|
return http_delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Files = new FileSystem();
|
||||||
|
export const Session = new SessionManager();
|
||||||
78
src/data/events.js
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// cheap event system that handle subscription, unsubscriptions and event emitions
|
||||||
|
import React from 'react';
|
||||||
|
let emitters = {}
|
||||||
|
|
||||||
|
function subscribe(key, event, fn){
|
||||||
|
if(emitters[event]){
|
||||||
|
emitters[event][key] = fn;
|
||||||
|
}else{
|
||||||
|
emitters[event] = {};
|
||||||
|
emitters[event][key] = fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribe(key, event){
|
||||||
|
if(emitters[event]){
|
||||||
|
if(key){
|
||||||
|
delete emitters[event][key];
|
||||||
|
}else{
|
||||||
|
delete emitters[event];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit(event, payload){
|
||||||
|
// trigger events if needed
|
||||||
|
if(emitters[event]){
|
||||||
|
return Promise.all(Object.keys(emitters[event]).map((key) => {
|
||||||
|
return emitters[event][key].apply(null, payload)
|
||||||
|
})).then((res) => {
|
||||||
|
return emitters[event] ? Promise.resolve(res) : Promise.reject({message: 'do not exist', code: 'CANCELLED'})
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
return Promise.reject({message: 'oups, something went wrong', code: 'NO_LISTENERS'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function EventReceiver(WrappedComponent){
|
||||||
|
let id = Math.random().toString();
|
||||||
|
|
||||||
|
return class extends React.Component {
|
||||||
|
subscribe(event, callback){
|
||||||
|
subscribe(id, event, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(event){
|
||||||
|
unsubscribe(id, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return <WrappedComponent subscribe={this.subscribe} unsubscribe={this.unsubscribe} {...this.props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function EventEmitter(WrappedComponent) {
|
||||||
|
return class extends React.Component {
|
||||||
|
emit(){
|
||||||
|
// reconstruct arguments
|
||||||
|
let args = Array.prototype.slice.call(arguments);
|
||||||
|
let event = args.shift();
|
||||||
|
let payload = args;
|
||||||
|
|
||||||
|
let res = emit(event, payload);
|
||||||
|
if(res.then){
|
||||||
|
return res;
|
||||||
|
}else{
|
||||||
|
return Promise.resolve(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <WrappedComponent emit={this.emit} {...this.props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/data/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export { Files } from './api';
|
||||||
|
export { Session } from './api';
|
||||||
|
export { invalidate } from './tools';
|
||||||
|
export { opener } from './mimetype';
|
||||||
|
export { EventEmitter, EventReceiver } from './events';
|
||||||
|
export { password } from './password';
|
||||||
218
src/data/mimetype.js
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
import Path from 'path';
|
||||||
|
|
||||||
|
export function getMimeType(file){
|
||||||
|
let ext = Path.extname(file).replace(/^\./, '').toLowerCase();
|
||||||
|
let mime = db[ext];
|
||||||
|
if(mime){
|
||||||
|
return mime;
|
||||||
|
}else{
|
||||||
|
return 'text/plain';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function opener(file){
|
||||||
|
let mime = getMimeType(file);
|
||||||
|
if(mime.split('/')[0] === 'text'){
|
||||||
|
return 'editor';
|
||||||
|
}else if(mime === 'application/pdf'){
|
||||||
|
return 'pdf';
|
||||||
|
}else if(mime.split('/')[0] === 'image'){
|
||||||
|
return 'image';
|
||||||
|
}else if(['application/javascript', 'application/xml'].indexOf(mime) !== -1){
|
||||||
|
return 'editor';
|
||||||
|
}else if(['audio/wav', 'audio/mp3', 'audio/flac'].indexOf(mime) !== -1){
|
||||||
|
return 'audio';
|
||||||
|
}else if(['video/webm', 'video/mp4', 'application/ogg'].indexOf(mime) !== -1){
|
||||||
|
return 'video';
|
||||||
|
}else{
|
||||||
|
return 'download';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const db = {
|
||||||
|
'pdf': 'application/pdf',
|
||||||
|
'csv': 'text/csv',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'bmp': 'image/bmp',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'svg': 'image/svg',
|
||||||
|
'png': 'image/png',
|
||||||
|
'ico': 'image/x-icon',
|
||||||
|
'cab': 'application/vnd.ms-cab-compressed',
|
||||||
|
'mpeg': 'audio/mpeg',
|
||||||
|
'mpg': 'audio/mpeg',
|
||||||
|
'aif': 'audio/x-aiff',
|
||||||
|
'aiff': 'audio/x-aiff',
|
||||||
|
'ra': 'audio/x-pn-realaudio',
|
||||||
|
'ram': 'audio/x-pn-realaudio',
|
||||||
|
'wav': 'audio/wave',
|
||||||
|
'mp3': 'audio/mp3',
|
||||||
|
'flac': 'audio/flac',
|
||||||
|
'wma': 'audio/x-ms-wma',
|
||||||
|
'wmv': 'video/x-ms-wmv',
|
||||||
|
'webm': 'video/webm',
|
||||||
|
'mp4': 'video/mp4',
|
||||||
|
'mov': 'video/quicktime',
|
||||||
|
'avi': 'video/x-msvideo',
|
||||||
|
'ogg': 'application/ogg',
|
||||||
|
'ogv': 'application/ogg',
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'xml': 'application/xml',
|
||||||
|
'deb': 'application/vnd.debian.binary-package',
|
||||||
|
'dpkg': 'application/dpkg-www-installer',
|
||||||
|
'rpm': 'application/x-rpm',
|
||||||
|
'apk': 'application/vnd.android.package-archive',
|
||||||
|
'exe': 'application/x-msdownload',
|
||||||
|
'msi': 'application/x-msdownload',
|
||||||
|
'dmg': 'application/x-apple-diskimage',
|
||||||
|
'pkg': 'application/x-newton-compatible-pkg',
|
||||||
|
'tar': 'application/x-tar',
|
||||||
|
'zip': 'application/x-zip',
|
||||||
|
'gz': 'application/x-gzip',
|
||||||
|
'bz2': 'application/x-bz2',
|
||||||
|
'rar': 'application/x-rar-compressed',
|
||||||
|
'so': 'application/octet-stream',
|
||||||
|
'eps': 'application/postscript',
|
||||||
|
'ps': 'application/postscript',
|
||||||
|
'midi': 'application/x-midi',
|
||||||
|
'odg': 'application/vnd.oasis.opendocument.graphics',
|
||||||
|
'odp': 'application/vnd.oasis.opendocument.presentation',
|
||||||
|
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
||||||
|
'odt': 'application/vnd.oasis.opendocument.text',
|
||||||
|
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'doc': 'application/msword',
|
||||||
|
'pps': 'application/vnd.ms-powerpoint',
|
||||||
|
'ppt': 'application/vnd.ms-powerpoint',
|
||||||
|
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'rtf': 'application/rtf',
|
||||||
|
'swf': 'application/x-shockwave-flash',
|
||||||
|
'vrml': 'application/x-vrml',
|
||||||
|
'wrl': 'x-world/x-vrml',
|
||||||
|
'xls': 'application/vnd.ms-excel',
|
||||||
|
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'ds_store': 'application/octet-stream',
|
||||||
|
// FROM nginx
|
||||||
|
"html": "text/html",
|
||||||
|
"shtml": "text/html",
|
||||||
|
"htm": "text/html",
|
||||||
|
"css": "text/css",
|
||||||
|
"xml": "text/xml",
|
||||||
|
"atom": "application/atom+xml",
|
||||||
|
"rss": "application/rss+xml",
|
||||||
|
"mml": "text/mathml",
|
||||||
|
"txt": "text/plain",
|
||||||
|
"jad": "text/vnd.sun.j2me.app-descriptor",
|
||||||
|
"wml": "text/vnd.wap.wml",
|
||||||
|
"htc": "text/x-component",
|
||||||
|
"tif": "image/tiff",
|
||||||
|
"tiff": "image/tiff",
|
||||||
|
"wbmp": "image/vnd.wap.wbmp",
|
||||||
|
"ico": "image/x-icon",
|
||||||
|
"jng": "image/x-jng",
|
||||||
|
"bmp": "image/x-ms-bmp",
|
||||||
|
"svg": "image/svg+xml",
|
||||||
|
"svgz": "image/svg+xml",
|
||||||
|
"webp": "image/webp",
|
||||||
|
"woff": "application/font-woff",
|
||||||
|
"jar": "application/java-archive",
|
||||||
|
"ear": "application/java-archive",
|
||||||
|
"war": "application/java-archive",
|
||||||
|
"json": "application/json",
|
||||||
|
"hqx": "application/mac-binhex40",
|
||||||
|
"doc": "application/msword",
|
||||||
|
"ai": "application/postscript",
|
||||||
|
"m3u8": "application/vnd.apple.mpegurl",
|
||||||
|
"eot": "application/vnd.ms-fontobject",
|
||||||
|
"wmlc": "application/vnd.wap.wmlc",
|
||||||
|
"kml": "application/vnd.google-earth.kml+xml",
|
||||||
|
"kmz": "application/vnd.google-earth.kmz",
|
||||||
|
"7z": "application/x-7z-compressed",
|
||||||
|
"cco": "application/x-cocoa",
|
||||||
|
"jardiff": "application/x-java-archive-diff",
|
||||||
|
"jnlp": "application/x-java-jnlp-file",
|
||||||
|
"run": "application/x-makeself",
|
||||||
|
"pl": "application/x-perl",
|
||||||
|
"pm": "application/x-perl",
|
||||||
|
"prc": "application/x-pilot",
|
||||||
|
"pdb": "application/x-pilot",
|
||||||
|
"rar": "application/x-rar-compressed",
|
||||||
|
"rpm": "application/x-redhat-package-manager",
|
||||||
|
"sea": "application/x-sea",
|
||||||
|
"swf": "application/x-shockwave-flash",
|
||||||
|
"sit": "application/x-stuffit",
|
||||||
|
"tcl": "application/x-tcl",
|
||||||
|
"tk": "application/x-tcl",
|
||||||
|
"der": "application/x-x509-ca-cert",
|
||||||
|
"crt": "application/x-x509-ca-cert",
|
||||||
|
"pem": "application/x-x509-ca-cert",
|
||||||
|
"xpi": "application/x-xpinstall",
|
||||||
|
"xhtml": "application/xhtml+xml",
|
||||||
|
"xspf": "application/xspf+xml",
|
||||||
|
"zip": "application/zip",
|
||||||
|
"bin": "application/octet-stream",
|
||||||
|
"dll": "application/octet-stream",
|
||||||
|
"exe": "application/octet-stream",
|
||||||
|
"deb": "application/octet-stream",
|
||||||
|
"dmg": "application/octet-stream",
|
||||||
|
"iso": "application/octet-stream",
|
||||||
|
"img": "application/octet-stream",
|
||||||
|
"msi": "application/octet-stream",
|
||||||
|
"msm": "application/octet-stream",
|
||||||
|
"msp": "application/octet-stream",
|
||||||
|
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"mid": "audio/midi",
|
||||||
|
"kar": "audio/midi",
|
||||||
|
"midi": "audio/midi",
|
||||||
|
"ogg": "audio/ogg",
|
||||||
|
"m4a": "audio/x-m4a",
|
||||||
|
"ra": "audio/x-realaudio",
|
||||||
|
"swf": "application/x-shockwave-flash",
|
||||||
|
"sit": "application/x-stuffit",
|
||||||
|
"tcl": "application/x-tcl",
|
||||||
|
"tk": "application/x-tcl",
|
||||||
|
"der": "application/x-x509-ca-cert",
|
||||||
|
"crt": "application/x-x509-ca-cert",
|
||||||
|
"pem": "application/x-x509-ca-cert",
|
||||||
|
"xpi": "application/x-xpinstall",
|
||||||
|
"xhtml": "application/xhtml+xml",
|
||||||
|
"xspf": "application/xspf+xml",
|
||||||
|
"zip": "application/zip",
|
||||||
|
"bin": "application/octet-stream",
|
||||||
|
"dll": "application/octet-stream",
|
||||||
|
"exe": "application/octet-stream",
|
||||||
|
"deb": "application/octet-stream",
|
||||||
|
"dmg": "application/octet-stream",
|
||||||
|
"iso": "application/octet-stream",
|
||||||
|
"img": "application/octet-stream",
|
||||||
|
"msi": "application/octet-stream",
|
||||||
|
"msm": "application/octet-stream",
|
||||||
|
"msp": "application/octet-stream",
|
||||||
|
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"mid": "audio/midi",
|
||||||
|
"kar": "audio/midi",
|
||||||
|
"midi": "audio/midi",
|
||||||
|
"ogg": "audio/ogg",
|
||||||
|
"m4a": "audio/x-m4a",
|
||||||
|
"ra": "audio/x-realaudio",
|
||||||
|
"3gpp": "video/3gpp",
|
||||||
|
"3gp": "video/3gpp",
|
||||||
|
"ts": "video/mp2t",
|
||||||
|
"mp4": "video/mp4",
|
||||||
|
"mpg": "video/mpeg",
|
||||||
|
"mov": "video/quicktime",
|
||||||
|
"webm": "video/webm",
|
||||||
|
"flv": "video/x-flv",
|
||||||
|
"m4v": "video/x-m4v",
|
||||||
|
"mng": "video/x-mng",
|
||||||
|
"asx": "video/x-ms-asf",
|
||||||
|
"asf": "video/x-ms-asf",
|
||||||
|
"wmv": "video/x-ms-wmv",
|
||||||
|
"avi": "video/x-msvideo"
|
||||||
|
}
|
||||||
14
src/data/password.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
function keyManager(){
|
||||||
|
let key = null;
|
||||||
|
return {
|
||||||
|
get: function(){
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
set: function(_key){
|
||||||
|
key = _key || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const password = keyManager();
|
||||||
140
src/data/tools.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
let cache = {};
|
||||||
|
|
||||||
|
// cleanup expired cache
|
||||||
|
setInterval(() => {
|
||||||
|
for(let key in cache){
|
||||||
|
if(cache[key].date < new Date().getTime()){
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 120*1000)
|
||||||
|
|
||||||
|
export function invalidate(url){
|
||||||
|
if(url === undefined){ cache = {}; }
|
||||||
|
else if(typeof url === 'string'){
|
||||||
|
if(cache[url]){
|
||||||
|
delete cache[url];
|
||||||
|
}
|
||||||
|
}else if(typeof url.exec === 'function'){ // regexp
|
||||||
|
for(let key in cache){
|
||||||
|
if(url.exec(key)){
|
||||||
|
delete cache[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
throw 'invalidation error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function http_get(url, cache_expire = 0, type = 'json'){
|
||||||
|
if(cache_expire > 0 && cache[url] && cache[url].date > new Date().getTime()){
|
||||||
|
return new Promise((done) => done(cache[url].data));
|
||||||
|
}else{
|
||||||
|
if(cache[url]){ delete cache[url]; }
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if(xhr.status === 200){
|
||||||
|
if(type === 'json'){
|
||||||
|
try{
|
||||||
|
let data = JSON.parse(xhr.responseText);
|
||||||
|
if(data.status === 'ok'){
|
||||||
|
if(cache_expire > 0){
|
||||||
|
cache[url] = {data: data.results || data.result, date: new Date().getTime() + cache_expire * 1000}
|
||||||
|
}
|
||||||
|
done(data.results || data.result)
|
||||||
|
}else{
|
||||||
|
err(data);
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
err({message: 'oups', trace: error})
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
done(xhr.responseText)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
xhr.send(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function http_post(url, data, type = 'json'){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", url, true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
if(type === 'json'){
|
||||||
|
data = JSON.stringify(data);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||||
|
}
|
||||||
|
xhr.send(data);
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if(xhr.status === 200){
|
||||||
|
try{
|
||||||
|
let data = JSON.parse(xhr.responseText);
|
||||||
|
data.status === 'ok' ? done(data.results || data.result) : err(data);
|
||||||
|
}catch(error){
|
||||||
|
err({message: 'oups', trace: error})
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function http_put(url, data){
|
||||||
|
// return new Promise((done, err) => {
|
||||||
|
// var xhr = new XMLHttpRequest();
|
||||||
|
// xhr.open("PUT", url, true);
|
||||||
|
// xhr.withCredentials = true;
|
||||||
|
// //xhr.setRequestHeader('Content-type','application/json; charset=utf-8');
|
||||||
|
// xhr.send(data);
|
||||||
|
// xhr.onload = function () {
|
||||||
|
// if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
// if(xhr.status === 200){
|
||||||
|
// try{
|
||||||
|
// let data = JSON.parse(xhr.responseText);
|
||||||
|
// data.status === 'ok' ? done(data.results || data.result) : err(data);
|
||||||
|
// }catch(error){
|
||||||
|
// err({message: 'oups', trace: error})
|
||||||
|
// }
|
||||||
|
// }else{
|
||||||
|
// err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'})
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function http_delete(url){
|
||||||
|
return new Promise((done, err) => {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("DELETE", url, true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if(xhr.status === 200){
|
||||||
|
try{
|
||||||
|
let data = JSON.parse(xhr.responseText);
|
||||||
|
data.status === 'ok' ? done(data.results || data.result) : err(data);
|
||||||
|
}catch(error){
|
||||||
|
err({message: 'oups', trace: error})
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.send(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
22
src/index.html
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
<meta content="yes" name="apple-mobile-web-app-capable">
|
||||||
|
<meta content="Nuage" name="apple-mobile-web-app-title">
|
||||||
|
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
|
||||||
|
<link rel="apple-touch-icon" href="/img/logo.png">
|
||||||
|
|
||||||
|
<title>Nuage</title>
|
||||||
|
<meta name="description" content="browse your files in the cloud">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
<link rel="stylesheet" href="/css/codemirror.css">
|
||||||
|
<link rel="stylesheet" href="/css/videojs-sublime-skin.css">
|
||||||
|
<link rel="stylesheet" href="/css/video-js.css">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
299
src/pages/connectpage.js
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Container, Card, NgIf, Input, Button, Textarea, Loader, Notification, encrypt, decrypt } from '../utilities';
|
||||||
|
import { Session, invalidate, password } from '../data';
|
||||||
|
import { Uploader } from '../utilities';
|
||||||
|
|
||||||
|
export class ConnectPage extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
type: 'webdav',
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
advanced_ftp: false, // state of checkbox in the UI
|
||||||
|
advanced_sftp: false, // state of checkbox in the UI
|
||||||
|
advanced_webdav: false,
|
||||||
|
advanced_s3: false,
|
||||||
|
credentials: {},
|
||||||
|
password: password.get() || null,
|
||||||
|
marginTop: this._marginTop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapt from: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||||
|
function getParam(name) {
|
||||||
|
const regex = new RegExp("[?&#]" + name.replace(/[\[\]]/g, "\\$&") + "(=([^&#]*)|&|#|$)");
|
||||||
|
const results = regex.exec(window.location.href);
|
||||||
|
if (!results) return null;
|
||||||
|
if (!results[2]) return '';
|
||||||
|
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropbox login
|
||||||
|
if(getParam('state') === 'dropbox'){
|
||||||
|
this.state.loading = true;
|
||||||
|
this.authenticate({bearer: getParam('access_token'), type: 'dropbox'})
|
||||||
|
}
|
||||||
|
// google drive login
|
||||||
|
if(getParam('code')){
|
||||||
|
this.state.loading = true;
|
||||||
|
this.authenticate({code: getParam('code'), type: 'gdrive'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_marginTop(){
|
||||||
|
let size = Math.round(Math.abs((document.body.offsetHeight - 300) / 2));
|
||||||
|
return size > 150? 150 : size;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
componentWillMount(){
|
||||||
|
window.onresize = () => {
|
||||||
|
this.setState({marginTop: this._marginTop()})
|
||||||
|
}
|
||||||
|
let raw = window.localStorage.getItem('store');
|
||||||
|
|
||||||
|
if(!this.state.loading && raw){
|
||||||
|
if(this.state.password === null){
|
||||||
|
let key = prompt("Your password: ");
|
||||||
|
if(key){
|
||||||
|
password.set(key);
|
||||||
|
let credentials = decrypt(raw, key);
|
||||||
|
this.setState({password: password, credentials: credentials}, setAdvanced.bind(this));
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
let credentials = decrypt(raw, this.state.password);
|
||||||
|
this.setState({credentials: credentials}, setAdvanced.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAdvanced(){
|
||||||
|
if(this.state.credentials['ftp'] && (this.state.credentials['ftp']['path'] || this.state.credentials['ftp']['port']) ){
|
||||||
|
this.setState({advanced_ftp: true})
|
||||||
|
}
|
||||||
|
if(this.state.credentials['sftp'] && (this.state.credentials['sftp']['path'] || this.state.credentials['sftp']['port'] || this.state.credentials['sftp']['private_key'])){
|
||||||
|
this.setState({advanced_sftp: true})
|
||||||
|
}
|
||||||
|
if(this.state.credentials['webdav'] && this.state.credentials['webdav']['path']){
|
||||||
|
this.setState({advanced_webdav: true})
|
||||||
|
}
|
||||||
|
if(this.state.credentials['s3'] && this.state.credentials['s3']['path']){
|
||||||
|
this.setState({advanced_s3: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefault(type, key){
|
||||||
|
if(this.state.credentials[type]){
|
||||||
|
return this.state.credentials[type][key]
|
||||||
|
}else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onRememberMe(e){
|
||||||
|
let value = e.target.checked;
|
||||||
|
if(value === true){
|
||||||
|
let key = prompt("password that will serve to encrypt your credentials:");
|
||||||
|
password.set(key);
|
||||||
|
this.setState({password: key});
|
||||||
|
}else if(value === false){
|
||||||
|
window.localStorage.clear();
|
||||||
|
password.set();
|
||||||
|
this.setState({credentials: {}, password: null});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(type){
|
||||||
|
this.setState({type: type});
|
||||||
|
}
|
||||||
|
|
||||||
|
login_dropbox(e){
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({loading: true});
|
||||||
|
Session.url('dropbox').then((url) => {
|
||||||
|
window.location.href = url;
|
||||||
|
}).catch((err) => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({loading: false, error: err});
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.setState({error: null})
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
login_google(e){
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({loading: true});
|
||||||
|
Session.url('gdrive').then((url) => {
|
||||||
|
window.location.href = url;
|
||||||
|
}).catch((err) => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({loading: false, error: err});
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.setState({error: null})
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate(params){
|
||||||
|
if(password.get()){
|
||||||
|
this.state.credentials[params['type']] = params;
|
||||||
|
window.localStorage.setItem('store', encrypt(this.state.credentials, password.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Session.authenticate(params)
|
||||||
|
.then((ok) => {
|
||||||
|
this.setState({loading: false});
|
||||||
|
invalidate();
|
||||||
|
const path = params.path && /^\//.test(params.path)? /\/$/.test(params.path) ? params.path : params.path+'/' : '/';
|
||||||
|
this.props.history.push('/files'+path);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({loading: false, error: err});
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.setState({error: null})
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(e){
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({loading: true});
|
||||||
|
|
||||||
|
// yes it's dirty but at least it's supported nearly everywhere and build won't push Megabytes or polyfill
|
||||||
|
// to support the entries method of formData which would have made things much cleaner
|
||||||
|
const serialize = function($form){
|
||||||
|
if(!$form) return {};
|
||||||
|
var obj = {};
|
||||||
|
var elements = $form.querySelectorAll( "input, select, textarea" );
|
||||||
|
for( var i = 0; i < elements.length; ++i ) {
|
||||||
|
var element = elements[i];
|
||||||
|
var name = element.name;
|
||||||
|
var value = element.value;
|
||||||
|
if(name){
|
||||||
|
obj[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
const data = serialize(document.querySelector('form'));
|
||||||
|
this.authenticate(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let labelStyle = {color: 'rgba(0,0,0,0.4)', fontStyle: 'italic', fontSize: '0.9em'}
|
||||||
|
let style = {
|
||||||
|
top: {minWidth: '80px', borderTopLeftRadius: 0, borderTopRightRadius: 0, padding: '8px 5px'}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{background: '#f89e6b'}}>
|
||||||
|
<ForkMe repo="https://github.com/mickael-kerjean/nuage" />
|
||||||
|
<Container maxWidth="500px">
|
||||||
|
<NgIf cond={this.state.loading === true}>
|
||||||
|
<Loader/>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.loading === false}>
|
||||||
|
<Card style={{marginTop: this.state.marginTop+'px', whiteSpace: '', borderRadius: '3px', boxShadow: 'none'}}>
|
||||||
|
<div style={{display: 'flex', margin: '-10px -11px 20px', padding: '0px 0px 6px 0'}} className={('ontouchstart' in window) ? 'scroll-x' : ''}>
|
||||||
|
<Button theme={this.state.type === 'webdav'? 'primary' : null} style={{...style.top, borderBottomLeftRadius: 0}} onClick={this.onChange.bind(this, 'webdav')}>WebDav</Button>
|
||||||
|
<Button theme={this.state.type === 'ftp'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 'ftp')}>FTP</Button>
|
||||||
|
<Button theme={this.state.type === 'sftp'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 'sftp')}>SFTP</Button>
|
||||||
|
<Button theme={this.state.type === 's3'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 's3')}>S3</Button>
|
||||||
|
<Button theme={this.state.type === 'dropbox'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 'dropbox')}>Dropbox</Button>
|
||||||
|
<Button theme={this.state.type === 'gdrive'? 'primary' : null} style={{...style.top, borderBottomRightRadius: 0}} onClick={this.onChange.bind(this, 'gdrive')}>Drive</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form onSubmit={this.onSubmit.bind(this)} autoComplete="off" autoCapitalize="off" spellCheck="false" autoCorrect="off">
|
||||||
|
<NgIf cond={this.state.type === 'webdav'}>
|
||||||
|
<Input type="text" name="url" placeholder="Address*" defaultValue={this.getDefault('webdav', 'url')} autoComplete="off" />
|
||||||
|
<Input type="text" name="username" placeholder="Username" defaultValue={this.getDefault('webdav', 'username')} autoComplete="off" />
|
||||||
|
<Input type="password" name="password" placeholder="Password" defaultValue={this.getDefault('webdav', 'password')} autoComplete="off" />
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<input checked={this.state.advanced_webdav} onChange={e => { this.setState({advanced_webdav: e.target.checked})}} type="checkbox" autoComplete="off"/> Advanced
|
||||||
|
</label>
|
||||||
|
<NgIf cond={this.state.advanced_webdav === true} style={{marginTop: '2px'}}>
|
||||||
|
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('webdav', 'path')} autoComplete="off" />
|
||||||
|
</NgIf>
|
||||||
|
<Input type="hidden" name="type" value="webdav"/>
|
||||||
|
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.type === 'ftp'}>
|
||||||
|
<Input type="text" name="hostname" placeholder="Hostname*" defaultValue={this.getDefault('ftp', 'hostname')} autoComplete="off" />
|
||||||
|
<Input type="text" name="username" placeholder="Username" defaultValue={this.getDefault('ftp', 'username')} autoComplete="off" />
|
||||||
|
<Input type="password" name="password" placeholder="Password" defaultValue={this.getDefault('ftp', 'password')} autoComplete="off" />
|
||||||
|
<Input type="hidden" name="type" value="ftp"/>
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<input checked={this.state.advanced_ftp} onChange={e => { this.setState({advanced_ftp: e.target.checked})}} type="checkbox" autoComplete="off"/> Advanced
|
||||||
|
</label>
|
||||||
|
<NgIf cond={this.state.advanced_ftp === true} style={{marginTop: '2px'}}>
|
||||||
|
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('ftp', 'path')} autoComplete="off" />
|
||||||
|
<Input type="text" name="port" placeholder="Port" defaultValue={this.getDefault('ftp', 'port')} autoComplete="off" />
|
||||||
|
</NgIf>
|
||||||
|
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.type === 'sftp'}>
|
||||||
|
<Input type="text" name="host" placeholder="Hostname*" defaultValue={this.getDefault('sftp', 'host')} autoComplete="off" />
|
||||||
|
<Input type="text" name="username" placeholder="Username" defaultValue={this.getDefault('sftp', 'username')} autoComplete="off" />
|
||||||
|
<Input type="password" name="password" placeholder="Password" defaultValue={this.getDefault('sftp', 'password')} autoComplete="off" />
|
||||||
|
<Input type="hidden" name="type" value="sftp"/>
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<input checked={this.state.advanced_sftp} onChange={e => { this.setState({advanced_sftp: JSON.parse(e.target.checked)})}} type="checkbox" autoComplete="off"/> Advanced
|
||||||
|
</label>
|
||||||
|
<NgIf cond={this.state.advanced_sftp === true} style={{marginTop: '2px'}}>
|
||||||
|
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('sftp', 'path')} autoComplete="off" />
|
||||||
|
<Input type="text" name="port" placeholder="Port" defaultValue={this.getDefault('sftp', 'port')} autoComplete="off" />
|
||||||
|
<Textarea type="text" name="private_key" placeholder="Private Key" defaultValue={this.getDefault('sftp', 'private_key')} autoComplete="off" />
|
||||||
|
</NgIf>
|
||||||
|
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.type === 's3'}>
|
||||||
|
<Input type="text" name="access_key_id" placeholder="Access Key ID*" defaultValue={this.getDefault('s3', 'access_key_id')} autoComplete="off" />
|
||||||
|
<Input type="password" name="secret_access_key" placeholder="Secret Access Key*" defaultValue={this.getDefault('s3', 'secret_access_key')} autoComplete="off" />
|
||||||
|
<Input type="hidden" name="type" value="s3"/>
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<input checked={this.state.advanced_s3} onChange={e => { this.setState({advanced_s3: JSON.parse(e.target.checked)})}} type="checkbox" autoComplete="off"/> Advanced
|
||||||
|
</label>
|
||||||
|
<NgIf cond={this.state.advanced_s3 === true} style={{marginTop: '2px'}}>
|
||||||
|
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('s3', 'path')} autoComplete="off" />
|
||||||
|
</NgIf>
|
||||||
|
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.type === 'dropbox'}>
|
||||||
|
<a target="_blank" href={this.state.dropbox_url}>
|
||||||
|
<div style={{textAlign: 'center'}} onClick={this.login_dropbox.bind(this)}>
|
||||||
|
<img src="/img/dropbox.png" style={{height: '115px', margin: '10px 0'}}/>
|
||||||
|
</div>
|
||||||
|
<Input type="hidden" name="type" value="dropbox"/>
|
||||||
|
<Button onClick={this.login_dropbox.bind(this)} style={{color: 'white'}} theme="emphasis">LOGIN WITH DROPBOX</Button>
|
||||||
|
</a>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.type === 'gdrive'}>
|
||||||
|
<div style={{textAlign: 'center'}} onClick={this.login_google.bind(this)}>
|
||||||
|
<img src="/img/google-drive.png" style={{height: '115px', margin: '10px 0'}}/>
|
||||||
|
</div>
|
||||||
|
<Input type="hidden" name="type" value="gdrive"/>
|
||||||
|
<Button onClick={this.login_google.bind(this)} style={{color: 'white'}} theme="emphasis">LOGIN WITH GOOGLE</Button>
|
||||||
|
</NgIf>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<label style={{ ...labelStyle, display: 'inline-block', width: '100%', textAlign: 'right'}}>
|
||||||
|
<input checked={this.state.password !== null} onChange={this.onRememberMe.bind(this)} type="checkbox"/> Remember me
|
||||||
|
</label>
|
||||||
|
</NgIf>
|
||||||
|
<Notification error={this.state.error && this.state.error.message} />
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ForkMe = (props) => {
|
||||||
|
return (
|
||||||
|
<a href={props.repo} target="_blank">
|
||||||
|
<img style={{position: 'absolute', top: 0, right: 0, border: 0}} src="https://camo.githubusercontent.com/52760788cde945287fbb584134c4cbc2bc36f904/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png" />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
211
src/pages/filespage.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FileSystem } from '../components/';
|
||||||
|
import BreadCrumb from './filespage/breadcrumb';
|
||||||
|
import { Files, EventReceiver } from '../data/';
|
||||||
|
import { NgIf, Loader, Error, debounce, goToFiles, goToViewer, Uploader } from '../utilities';
|
||||||
|
|
||||||
|
import { DragDropContext } from 'react-dnd';
|
||||||
|
import HTML5Backend from 'react-dnd-html5-backend';
|
||||||
|
import { default as TouchBackend } from 'react-dnd-touch-backend';
|
||||||
|
import Path from 'path';
|
||||||
|
|
||||||
|
|
||||||
|
@EventReceiver
|
||||||
|
@DragDropContext(('ontouchstart' in window)? HTML5Backend : HTML5Backend)
|
||||||
|
export class FilesPage extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
path: props.match.url.replace('/files', '') || '/',
|
||||||
|
files: [],
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
height: null
|
||||||
|
};
|
||||||
|
this.resetHeight = debounce(this.resetHeight.bind(this), 100);
|
||||||
|
this.goToFiles = goToFiles.bind(null, this.props.history);
|
||||||
|
this.goToViewer = goToViewer.bind(null, this.props.history);
|
||||||
|
|
||||||
|
// subscriptions
|
||||||
|
this.props.subscribe('file.select', this.onPathUpdate.bind(this));
|
||||||
|
this.props.subscribe('file.upload', this.onUpload.bind(this));
|
||||||
|
this.props.subscribe('file.create', this.onCreate.bind(this));
|
||||||
|
this.props.subscribe('file.rename', this.onRename.bind(this));
|
||||||
|
this.props.subscribe('file.delete', this.onDelete.bind(this));
|
||||||
|
this.props.subscribe('file.refresh', this.onRefresh.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
componentWillMount(){
|
||||||
|
this.onPathUpdate(this.state.path, 'directory', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.unsubscribe('file.select');
|
||||||
|
this.props.unsubscribe('file.upload');
|
||||||
|
this.props.unsubscribe('file.create');
|
||||||
|
this.props.unsubscribe('file.rename');
|
||||||
|
this.props.unsubscribe('file.delete');
|
||||||
|
this.props.unsubscribe('file.refresh');
|
||||||
|
window.removeEventListener("resize", this.resetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
this.resetHeight();
|
||||||
|
window.addEventListener("resize", this.resetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onRefresh(path = this.state.path){
|
||||||
|
return Files.ls(path).then((files) => {
|
||||||
|
this.setState({files: files, loading: false})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onPathUpdate(path, type = 'directory', withLoader = true){
|
||||||
|
window.path = this.props.history;
|
||||||
|
if(type === 'file'){
|
||||||
|
this.props.history.push('/view'+path)
|
||||||
|
}else{
|
||||||
|
this.setState({path: path, loading: withLoader});
|
||||||
|
if(path !== this.state.path){
|
||||||
|
this.props.history.push('/files'+path)
|
||||||
|
}
|
||||||
|
return this.onRefresh(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreate(path, type, file){
|
||||||
|
if(type === 'file'){
|
||||||
|
return Files.touch(path, file);
|
||||||
|
}else if(type === 'directory'){
|
||||||
|
return Files.mkdir(path);
|
||||||
|
}else{
|
||||||
|
return Promise.reject({message: 'internal error: can\'t create a '+type.toString(), code: 'UNKNOWN_TYPE'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onRename(from, to, type){
|
||||||
|
return Files.mv(from, to, type);
|
||||||
|
}
|
||||||
|
onDelete(file, type){
|
||||||
|
return Files.rm(file, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onUpload(path, files){
|
||||||
|
const createFilesInUI = (_files) => {
|
||||||
|
const newfiles = _files.map((file) => {
|
||||||
|
return {
|
||||||
|
time: new Date().getTime(),
|
||||||
|
name: file.name,
|
||||||
|
type: 'file',
|
||||||
|
size: file.size,
|
||||||
|
icon: 'loading',
|
||||||
|
virtual: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const files = JSON.parse(JSON.stringify(this.state.files));
|
||||||
|
this.setState({files: [].concat(newfiles, files)});
|
||||||
|
return Promise.resolve(_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
const processFile = (file) => {
|
||||||
|
return this.onCreate(Path.join(path, file.name), 'file', file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = (filename) => {
|
||||||
|
const files = JSON.parse(JSON.stringify(this.state.files))
|
||||||
|
.map((file) => {
|
||||||
|
if(file.name === filename){
|
||||||
|
file.virtual = false;
|
||||||
|
delete file.icon;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
this.setState({files: files});
|
||||||
|
return Promise.resolve('ok')
|
||||||
|
}
|
||||||
|
|
||||||
|
const showError = (filename, err) => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
const files = JSON.parse(JSON.stringify(this.state.files))
|
||||||
|
.map((file) => {
|
||||||
|
if(file.name === filename){
|
||||||
|
file.icon = 'error';
|
||||||
|
file.message = err && err.message || 'oups something went wrong';
|
||||||
|
file.virtual = true;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
this.setState({files: files});
|
||||||
|
return Promise.resolve('ok')
|
||||||
|
}
|
||||||
|
|
||||||
|
function generator(arr){
|
||||||
|
let store = arr;
|
||||||
|
return {
|
||||||
|
next: function(){
|
||||||
|
return store.pop()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function job(it){
|
||||||
|
let file = it.next();
|
||||||
|
if(file){
|
||||||
|
return processFile(file)
|
||||||
|
.then((ok) => updateUI(file.name))
|
||||||
|
.then(() => job(it))
|
||||||
|
.catch((err) => showError(file.name, err))
|
||||||
|
}else{
|
||||||
|
return Promise.resolve('ok');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function process(it, pool){
|
||||||
|
return Promise.all(Array.apply(null, Array(pool)).map(() => {
|
||||||
|
return job(it);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const poolSize = 10;
|
||||||
|
return createFilesInUI(files)
|
||||||
|
.then((files) => Promise.resolve(generator(files)))
|
||||||
|
.then((it) => process(it, poolSize))
|
||||||
|
.then((res) => Promise.resolve('ok'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
resetHeight(){
|
||||||
|
this.setState({
|
||||||
|
height: document.body.clientHeight - document.querySelector('.breadcrumb').offsetHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BreadCrumb className="breadcrumb" path={this.state.path} />
|
||||||
|
<div style={{height: this.state.height+'px'}} className="scroll-y">
|
||||||
|
<NgIf cond={!this.state.loading} style={{padding: '5px 0 20px 0', height: '100%', boxSizing: 'border-box'}}>
|
||||||
|
<FileSystem path={this.state.path} files={this.state.files} />
|
||||||
|
<Uploader path={this.state.path} />
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.loading}>
|
||||||
|
<NgIf cond={this.state.error === false}>
|
||||||
|
<Loader/>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.error !== false}>
|
||||||
|
<Error err={this.state.error}/>
|
||||||
|
</NgIf>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
69
src/pages/filespage/breadcrumb.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { EventEmitter } from '../../data';
|
||||||
|
import { BreadCrumb, PathElement } from '../../components/breadcrumb';
|
||||||
|
import { pathBuilder } from '../../utilities';
|
||||||
|
import { DropTarget } from 'react-dnd';
|
||||||
|
|
||||||
|
|
||||||
|
export default class BreadCrumbTargettable extends BreadCrumb{
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return super.render(Element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BreadCrumbTargettable.PropTypes = {
|
||||||
|
path: PropTypes.string.isRequred
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fileTarget = {
|
||||||
|
canDrop(props){
|
||||||
|
return props.isLast ? false : true;
|
||||||
|
},
|
||||||
|
drop(props, monitor, component){
|
||||||
|
let src = monitor.getItem();
|
||||||
|
let from = pathBuilder(src.path, src.name, src.type);
|
||||||
|
let to = pathBuilder(props.path.full, src.name, src.type);
|
||||||
|
return {action: 'rename', args: [from, to, src.type], ctx: 'breadcrumb'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nativeFileTarget = {
|
||||||
|
canDrop(props){
|
||||||
|
return props.isLast ? false : true;
|
||||||
|
},
|
||||||
|
drop: function(props, monitor){
|
||||||
|
let files = monitor.getItem().files;
|
||||||
|
props.emit('file.upload', props.path.full, files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventEmitter
|
||||||
|
@DropTarget('__NATIVE_FILE__', nativeFileTarget, (connect, monitor) => ({
|
||||||
|
connectNativeFileDropTarget: connect.dropTarget(),
|
||||||
|
isNativeFileOver: monitor.isOver(),
|
||||||
|
canDropFile: monitor.canDrop()
|
||||||
|
}))
|
||||||
|
@DropTarget('file', fileTarget, (connect, monitor) => ({
|
||||||
|
connectDropTarget: connect.dropTarget(),
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
canDrop: monitor.canDrop()
|
||||||
|
}))
|
||||||
|
class Element extends PathElement {
|
||||||
|
constructor(props){
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
let highlight = (this.props.isOver && this.props.canDrop ) || (this.props.isNativeFileOver && this.props.canDropFile);
|
||||||
|
return this.props.connectNativeFileDropTarget(this.props.connectDropTarget(
|
||||||
|
super.render(highlight)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Element.PropTypes = {
|
||||||
|
path: PropTypes.string.isRequred
|
||||||
|
}
|
||||||
306
src/pages/filespage/existingthing.js
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Card, NgIf, Icon, pathBuilder } from '../../utilities';
|
||||||
|
import { EventEmitter } from '../../data';
|
||||||
|
import { DragSource, DropTarget } from 'react-dnd';
|
||||||
|
|
||||||
|
const fileSource = {
|
||||||
|
beginDrag(props, monitor, component) {
|
||||||
|
return {
|
||||||
|
path: props.path,
|
||||||
|
name: props.file.name,
|
||||||
|
type: props.file.type
|
||||||
|
};
|
||||||
|
},
|
||||||
|
canDrag(props, monitor){
|
||||||
|
// would have been great to use forbid dragging while there's some actions happenning
|
||||||
|
// but react-dnd won't give us the component in argument :(
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
endDrag(props, monitor, component){
|
||||||
|
if(monitor.didDrop() && component.state.icon !== 'loading'){
|
||||||
|
let result = monitor.getDropResult();
|
||||||
|
if(result.action === 'rename'){
|
||||||
|
component.setState({icon: 'loading', message: null}, function(){
|
||||||
|
props.emit.apply(component, ['file.rename'].concat(result.args))
|
||||||
|
.then((ok) => {
|
||||||
|
component.setState({appear: false})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
component.setState({icon: 'error', message: err.message})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
throw 'unknown action'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileTarget = {
|
||||||
|
canDrop(props, monitor){
|
||||||
|
let file = monitor.getItem();
|
||||||
|
if(props.file.type === 'directory' && file.name !== props.file.name){
|
||||||
|
return true;
|
||||||
|
}else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drop(props, monitor, component){
|
||||||
|
let src = monitor.getItem();
|
||||||
|
let dest = props.file;
|
||||||
|
|
||||||
|
let from = pathBuilder(props.path, src.name, src.type);
|
||||||
|
let to = pathBuilder(props.path, './'+dest.name+'/'+src.name, src.type);
|
||||||
|
return {action: 'rename', args: [from, to, src.type], ctx: 'existingfile'}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nativeFileTarget = {
|
||||||
|
canDrop: fileTarget.canDrop,
|
||||||
|
drop(props, monitor){
|
||||||
|
let files = monitor.getItem().files;
|
||||||
|
let path = pathBuilder(props.path, props.file.name, 'directory');
|
||||||
|
props.emit('file.upload', path, files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventEmitter
|
||||||
|
@DropTarget('__NATIVE_FILE__', nativeFileTarget, (connect, monitor) => ({
|
||||||
|
connectDropNativeFile: connect.dropTarget(),
|
||||||
|
nativeFileIsOver: monitor.isOver(),
|
||||||
|
canDropNativeFile: monitor.canDrop()
|
||||||
|
}))
|
||||||
|
@DropTarget('file', fileTarget, (connect, monitor) => ({
|
||||||
|
connectDropFile: connect.dropTarget(),
|
||||||
|
fileIsOver: monitor.isOver(),
|
||||||
|
canDropFile: monitor.canDrop()
|
||||||
|
}))
|
||||||
|
@DragSource('file', fileSource, (connect, monitor) => ({
|
||||||
|
connectDragSource: connect.dragSource(),
|
||||||
|
isDragging: monitor.isDragging()
|
||||||
|
}))
|
||||||
|
export class ExistingThing extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
appear: true,
|
||||||
|
hover: null,
|
||||||
|
message: null,
|
||||||
|
icon: props.file.type,
|
||||||
|
filename: props.file.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(props){
|
||||||
|
this.setState({
|
||||||
|
filename: props.file.name,
|
||||||
|
message: props.file.message || null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onSelect(){
|
||||||
|
if(this.state.icon !== 'loading'){
|
||||||
|
this.props.emit('file.select', pathBuilder(this.props.path, this.props.file.name, this.props.file.type), this.props.file.type)
|
||||||
|
.catch((err) => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({icon: 'error', message: err.message});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRename(newFilename){
|
||||||
|
let oldFilename = this.props.file.name;
|
||||||
|
this.setState({icon: 'loading', filename: newFilename});
|
||||||
|
this.props.emit(
|
||||||
|
'file.rename',
|
||||||
|
pathBuilder(this.props.path, oldFilename),
|
||||||
|
pathBuilder(this.props.path, newFilename),
|
||||||
|
this.props.file.type
|
||||||
|
)
|
||||||
|
.then((ok) => this.props.emit('file.refresh', this.props.path))
|
||||||
|
.catch((err) => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({icon: 'error', message: err.message, filename: oldFilename});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete(filename){
|
||||||
|
let toConfirm = this.props.file.name.length > 16? this.props.file.name.substring(0, 10).toLowerCase() : this.props.file.name;
|
||||||
|
let answer = prompt('Confirm by tapping "'+toConfirm+'"');
|
||||||
|
if(answer === toConfirm){
|
||||||
|
this.setState({icon: 'loading'});
|
||||||
|
this.props.emit(
|
||||||
|
'file.delete',
|
||||||
|
pathBuilder(this.props.path, this.props.file.name),
|
||||||
|
this.props.file.type
|
||||||
|
).then((ok) => {
|
||||||
|
this.setState({appear: false})
|
||||||
|
}).catch((err) => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({icon: 'error', message: err.message});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render(highlight){
|
||||||
|
const { connectDragSource, connectDropFile, connectDropNativeFile } = this.props;
|
||||||
|
let dragStyle = {whiteSpace: 'nowrap'};
|
||||||
|
if(this.props.isDragging) { dragStyle.opacity = 0.15; }
|
||||||
|
if((this.props.fileIsOver && this.props.canDropFile) || (this.props.nativeFileIsOver && this.props.canDropNativeFile)) {
|
||||||
|
dragStyle.background = 'rgba(209, 255, 255,0.5)';
|
||||||
|
dragStyle.border = '2px solid #38a6a6';
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectDragSource(connectDropNativeFile(connectDropFile(
|
||||||
|
<div>
|
||||||
|
<NgIf cond={this.state.appear}>
|
||||||
|
<Card onClick={this.onSelect.bind(this)} onMouseEnter={() => this.setState({hover: true})} onMouseLeave={() => this.setState({hover: false})} style={dragStyle}>
|
||||||
|
<DateTime show={this.state.hover !== true || this.state.icon === 'loading'} timestamp={this.props.file.time}/>
|
||||||
|
<Updater filename={this.state.filename}
|
||||||
|
icon={this.props.file.virtual? this.props.file.icon : this.state.icon}
|
||||||
|
can_move={this.props.file.can_move !== false}
|
||||||
|
can_delete={this.props.file.can_delete !== false}
|
||||||
|
show={this.state.hover === true && this.state.icon !== 'loading' && !('ontouchstart' in window)}
|
||||||
|
onRename={this.onRename.bind(this)}
|
||||||
|
onDelete={this.onDelete.bind(this)} />
|
||||||
|
<FileSize type={this.props.file.type} size={this.props.file.size} />
|
||||||
|
<Message message={this.state.message} />
|
||||||
|
</Card>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExistingThing.PropTypes = {
|
||||||
|
connectDragSource: PropTypes.func.isRequired,
|
||||||
|
isDragging: PropTypes.bool.isRequired,
|
||||||
|
fileIsOver: PropTypes.bool.isRequired,
|
||||||
|
nativeFileIsOver: PropTypes.bool.isRequired,
|
||||||
|
canDropFile: PropTypes.bool.isRequired,
|
||||||
|
canDropNativeFile: PropTypes.bool.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
class Updater extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
editing: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRename(e){
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onRename(this.state.editing);
|
||||||
|
this.setState({editing: null})
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete(e){
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.onDelete()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onRenameRequest(e){
|
||||||
|
e.stopPropagation();
|
||||||
|
if(this.state.editing === null){
|
||||||
|
this.setState({editing: this.props.filename});
|
||||||
|
}else{
|
||||||
|
this.setState({editing: null});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
preventSelect(e){
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
const style = {
|
||||||
|
inline: {display: 'inline'},
|
||||||
|
el: {float: 'right', color: '#6f6f6f', height: '22px', background: 'white', margin: '0 -10px', padding: '0 10px', position: 'relative'},
|
||||||
|
margin: {marginRight: '10px'}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{display: 'inline'}}>
|
||||||
|
<NgIf cond={this.props.show} style={style.el}>
|
||||||
|
<NgIf cond={this.props.can_move} style={style.inline}>
|
||||||
|
<Icon name="edit" onClick={this.onRenameRequest.bind(this)} style={style.margin} style={{width: '25px', height: '25px'}} />
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.props.can_delete !== false} style={style.inline}>
|
||||||
|
<Icon name="delete" onClick={this.onDelete.bind(this)} style={{width: '25px', height: '25px'}} />
|
||||||
|
</NgIf>
|
||||||
|
</NgIf>
|
||||||
|
<Icon style={{width: '25px', height: '25px'}} name={this.props.icon} />
|
||||||
|
<span style={{padding: '5px', lineHeight: '22px'}}>
|
||||||
|
<NgIf style={{display: 'inline'}} cond={this.state.editing === null}>{this.props.filename}</NgIf>
|
||||||
|
<NgIf style={{display: 'inline'}} cond={this.state.editing !== null}>
|
||||||
|
<form onClick={this.preventSelect} onSubmit={this.onRename.bind(this)} style={{display: 'inline'}}>
|
||||||
|
<input value={this.state.editing} onChange={(e) => this.setState({editing: e.target.value})} autoFocus />
|
||||||
|
</form>
|
||||||
|
</NgIf>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateTime = (props) => {
|
||||||
|
function displayTime(timestamp){
|
||||||
|
function padding(number){
|
||||||
|
let str = String(number),
|
||||||
|
pad = "00";
|
||||||
|
return pad.substring(0, pad.length - str.length) + str
|
||||||
|
}
|
||||||
|
if(timestamp){
|
||||||
|
let t = new Date(timestamp);
|
||||||
|
return padding(t.getDate()) + '/'+ padding(t.getMonth()) + '/' + padding(t.getFullYear());
|
||||||
|
}else{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {float: 'right', color: '#6f6f6f', lineHeight: '25px', background: 'white', margin: '0 -10px', padding: '0 10px', position: 'relative'};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NgIf cond={props.show} style={style}>
|
||||||
|
<span>{displayTime(props.timestamp)}</span>
|
||||||
|
</NgIf>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileSize = (props) => {
|
||||||
|
function displaySize(bytes){
|
||||||
|
if(bytes < 1024){
|
||||||
|
return bytes+'B';
|
||||||
|
}else if(bytes < 1048576){
|
||||||
|
return Math.round(bytes/1024*10)/10+'KB';
|
||||||
|
}else if(bytes < 1073741824){
|
||||||
|
return Math.round(bytes/(1024*1024)*10)/10+'MB';
|
||||||
|
}else if(bytes < 1099511627776){
|
||||||
|
return Math.round(bytes/(1024*1024*1024)*10)/10+'GB';
|
||||||
|
}else{
|
||||||
|
return Math.round(bytes/(1024*1024*1024*1024))+'TB'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const style = {color: '#6f6f6f', fontSize: '0.85em'};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NgIf cond={props.type === 'file'} style={{display: 'inline-block'}}>
|
||||||
|
<span style={style}>({displaySize(props.size)})</span>
|
||||||
|
</NgIf>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const Message = (props) => {
|
||||||
|
const style = {color: 'rgba(0,0,0,0.4)', fontSize: '0.9em', paddingLeft: '10px', display: 'inline'};
|
||||||
|
return (
|
||||||
|
<NgIf cond={props.message !== null} style={style}>
|
||||||
|
- {props.message}
|
||||||
|
</NgIf>
|
||||||
|
)
|
||||||
|
}
|
||||||
49
src/pages/filespage/filezone.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { DropTarget } from 'react-dnd';
|
||||||
|
import { EventEmitter } from '../../data';
|
||||||
|
|
||||||
|
|
||||||
|
@EventEmitter
|
||||||
|
@DropTarget('__NATIVE_FILE__', {
|
||||||
|
drop(props, monitor){
|
||||||
|
let files = monitor.getItem().files
|
||||||
|
props.emit('file.upload', props.path, files)
|
||||||
|
.then((ok) => {
|
||||||
|
console.log("DONE", ok)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("ERROR", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, (connect, monitor) => ({
|
||||||
|
connectDropFile: connect.dropTarget(),
|
||||||
|
fileIsOver: monitor.isOver()
|
||||||
|
}))
|
||||||
|
export class FileZone extends React.Component{
|
||||||
|
constructor(props){
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
let style = {
|
||||||
|
border: '2px dashed',
|
||||||
|
padding: '25px 0',
|
||||||
|
marginBottom: '10px',
|
||||||
|
textAlign: 'center'
|
||||||
|
}
|
||||||
|
if(this.props.fileIsOver){
|
||||||
|
style.background = 'rgba(209, 255, 255,0.5)';
|
||||||
|
style.border = '2px dashed #38a6a6';
|
||||||
|
}
|
||||||
|
return this.props.connectDropFile(
|
||||||
|
<div style={style}>
|
||||||
|
DROP HERE TO UPLOAD
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileZone.PropTypes = {
|
||||||
|
path: PropTypes.string.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
3
src/pages/filespage/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { NewThing } from './newthing';
|
||||||
|
export { ExistingThing } from './existingthing';
|
||||||
|
export { FileZone } from './filezone';
|
||||||
84
src/pages/filespage/newthing.js
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { EventEmitter } from '../../data';
|
||||||
|
import { Card, NgIf, Icon, pathBuilder } from '../../utilities';
|
||||||
|
|
||||||
|
|
||||||
|
@EventEmitter
|
||||||
|
export class NewThing extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
name: null,
|
||||||
|
type: null,
|
||||||
|
message: null,
|
||||||
|
icon: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNew(type){
|
||||||
|
this.setState({type: type, name: '', icon: type})
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete(){
|
||||||
|
this.setState({type: null, name: null, icon: null})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(e){
|
||||||
|
e.preventDefault();
|
||||||
|
if(this.state.name !== null){
|
||||||
|
this.setState({icon: 'loading'})
|
||||||
|
this.props.emit('file.create', pathBuilder(this.props.path, this.state.name, this.state.type), this.state.type)
|
||||||
|
.then((ok) => this.props.emit('file.refresh', this.props.path))
|
||||||
|
.then((ok) => {
|
||||||
|
this.onDelete();
|
||||||
|
return Promise.resolve('ok');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({message: err.message, icon: 'error'})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortUpdate(e){
|
||||||
|
this.props.onSortUpdate(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{fontSize: '15px', lineHeight: '15px', height: '15px', marginTop: '5px', color: 'rgba(0,0,0,0.4)', margin: '0 0 10px 0'}}>
|
||||||
|
<NgIf cond={this.props.accessRight.can_create_file === true} onClick={this.onNew.bind(this, 'file')} style={{marginRight: '15px', cursor: 'pointer', display: 'inline'}}>New File</NgIf>
|
||||||
|
<NgIf cond={this.props.accessRight.can_create_directory === true} onClick={this.onNew.bind(this, 'directory')} style={{cursor: 'pointer', display: 'inline'}}>New Directory</NgIf>
|
||||||
|
<select value={this.props.sort} onChange={this.onSortUpdate.bind(this)} style={{float: 'right', color: 'rgba(0,0,0,0.4)', background: 'none', borderRadius: '5px', outline: 'none', border: '1px solid rgba(0,0,0,0.4)'}}>
|
||||||
|
<option value="type">Sort By Type</option>
|
||||||
|
<option value="date">Sort By Date</option>
|
||||||
|
<option value="name">Sort By Name</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<NgIf cond={this.state.type !== null}>
|
||||||
|
<Card>
|
||||||
|
<Icon style={{width: '25px', height: '25px'}} name={this.state.icon} />
|
||||||
|
<form onSubmit={this.onSave.bind(this)} style={{display: 'inline'}}>
|
||||||
|
<input onChange={(e) => this.setState({name: e.target.value})} value={this.state.name} style={{outline: 'none'}} type="text" autoFocus/>
|
||||||
|
</form>
|
||||||
|
<NgIf cond={this.state.message !== null} style={{color: 'rgba(0,0,0,0.4)', fontSize: '0.9em', paddingLeft: '10px', display: 'inline'}}>
|
||||||
|
{this.state.message}
|
||||||
|
</NgIf>
|
||||||
|
<div style={{float: 'right', height: '22px'}}>
|
||||||
|
<Icon style={{width: '25px', height: '25px'}} name="delete" onClick={this.onDelete.bind(this)} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
NewThing.PropTypes = {
|
||||||
|
accessRight: PropTypes.obj,
|
||||||
|
onCreate: PropTypes.func.isRequired,
|
||||||
|
onSortUpdate: PropTypes.func.isRequired,
|
||||||
|
sort: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
28
src/pages/homepage.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Session } from '../data';
|
||||||
|
import { Loader } from '../utilities';
|
||||||
|
|
||||||
|
export class HomePage extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
Session.isLogged()
|
||||||
|
.then((res) => {
|
||||||
|
if(res === true){
|
||||||
|
this.props.history.push('/files');
|
||||||
|
}else{
|
||||||
|
this.props.history.push('/login');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((res) => {
|
||||||
|
console.warn(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div> <Loader /> </div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/pages/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export { HomePage } from './homepage';
|
||||||
|
export { ConnectPage } from './connectpage';
|
||||||
|
export { LogoutPage } from './logout';
|
||||||
|
export { FilesPage } from './filespage';
|
||||||
|
export { ViewerPage } from './viewerpage';
|
||||||
|
export { NotFoundPage } from './notfound';
|
||||||
25
src/pages/logout.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Session, invalidate } from '../data';
|
||||||
|
import { Loader } from '../utilities';
|
||||||
|
|
||||||
|
export class LogoutPage extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
invalidate();
|
||||||
|
Session.logout()
|
||||||
|
.then((res) => {
|
||||||
|
this.props.history.push('/');
|
||||||
|
})
|
||||||
|
.catch((res) => {
|
||||||
|
console.warn(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div> <Loader /> </div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/pages/notfound.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
export class NotFoundPage extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="not-found">
|
||||||
|
<h1>404</h1>
|
||||||
|
<h2>Page not found!</h2>
|
||||||
|
<p>
|
||||||
|
<Link to="/">Go back to the main page</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
423
src/pages/viewerpage.js
Normal file
|
|
@ -0,0 +1,423 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { BreadCrumb, Editor } from '../components/';
|
||||||
|
import { debounce, throttle, NgIf, Loader, Error, Fab, Icon, Container } from '../utilities/';
|
||||||
|
import { Files, opener, EventReceiver, EventEmitter } from '../data/';
|
||||||
|
import Path from 'path';
|
||||||
|
import { theme } from '../utilities';
|
||||||
|
import WaveSurfer from 'wavesurfer.js';
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
|
||||||
|
@EventReceiver
|
||||||
|
export class ViewerPage extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
path: props.match.url.replace('/view', ''),
|
||||||
|
filename: Path.basename(props.match.url.replace('/view', '')) || 'untitled.dat',
|
||||||
|
opener: null,
|
||||||
|
data: '',
|
||||||
|
needSaving: false,
|
||||||
|
isSaving: false,
|
||||||
|
height: null,
|
||||||
|
loading: true,
|
||||||
|
error: false
|
||||||
|
};
|
||||||
|
this.resetHeight = debounce(this.resetHeight.bind(this), 100);
|
||||||
|
this.props.subscribe('file.select', this.onPathUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.unsubscribe('file.select')
|
||||||
|
window.removeEventListener("resize", this.resetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
save(file){
|
||||||
|
this.setState({isSaving: true})
|
||||||
|
Files.save(this.state.path, file)
|
||||||
|
.then(() => {
|
||||||
|
this.setState({isSaving: false})
|
||||||
|
this.setState({needSaving: false})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({isSaving: false})
|
||||||
|
let message = "Oups, something went wrong"
|
||||||
|
if(err.message){
|
||||||
|
message += ':\n'+err.message
|
||||||
|
}
|
||||||
|
alert(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onPathUpdate(path){
|
||||||
|
this.props.history.push('/files'+path)
|
||||||
|
}
|
||||||
|
|
||||||
|
needSaving(bool){
|
||||||
|
this.setState({needSaving: bool})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
this.resetHeight();
|
||||||
|
window.addEventListener("resize", this.resetHeight);
|
||||||
|
this.setState({loading: true});
|
||||||
|
let app = opener(this.state.path);
|
||||||
|
if(app === 'editor'){
|
||||||
|
Files.cat(this.state.path).then((content) => {
|
||||||
|
this.setState({data: content, loading: false, opener: app});
|
||||||
|
}).catch(err => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
if(err.code === 'BINARY_FILE'){
|
||||||
|
Files.url(this.state.path).then((url) => {
|
||||||
|
this.setState({data: url, loading: false, opener: 'download'});
|
||||||
|
}).catch(err => {
|
||||||
|
this.setState({error: err});
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
this.setState({error: err});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
Files.url(this.state.path).then((url) => {
|
||||||
|
this.setState({data: url, loading: false, opener: app});
|
||||||
|
}).catch(err => {
|
||||||
|
if(err && err.code === 'CANCELLED'){ return }
|
||||||
|
this.setState({error: err});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetHeight(){
|
||||||
|
this.setState({
|
||||||
|
height: document.body.clientHeight - document.querySelector('.breadcrumb').offsetHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let style = {height: '100%'};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BreadCrumb needSaving={this.state.needSaving} className="breadcrumb" path={this.state.path} />
|
||||||
|
<div style={{height: this.state.height ? this.state.height + 'px' : '100%'}}>
|
||||||
|
<NgIf cond={this.state.loading === false} style={{height: '100%'}}>
|
||||||
|
<NgIf cond={this.state.opener === 'editor'} style={style}>
|
||||||
|
<IDE needSaving={this.needSaving.bind(this)}
|
||||||
|
isSaving={this.state.isSaving}
|
||||||
|
onSave={this.save.bind(this)}
|
||||||
|
content={this.state.data}
|
||||||
|
filename={this.state.filename}/>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.opener === 'image'} style={style}>
|
||||||
|
<ImageViewer data={this.state.data} filename={this.state.filename} />
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.opener === 'pdf'} style={style}>
|
||||||
|
<PDFViewer data={this.state.data} filename={this.state.filename} />
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.opener === 'video'} style={style}>
|
||||||
|
<VideoPlayer data={this.state.data} filename={this.state.filename} />
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.opener === 'audio'} style={style}>
|
||||||
|
<AudioPlayer data={this.state.data} filename={this.state.filename} />
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.opener === 'download'} style={style}>
|
||||||
|
<FileDownloader data={this.state.data} filename={this.state.filename} />
|
||||||
|
</NgIf>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.loading === true}>
|
||||||
|
<NgIf cond={this.state.error === false}>
|
||||||
|
<Loader/>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.error !== false}>
|
||||||
|
<Error err={this.state.error}/>
|
||||||
|
</NgIf>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IDE extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
contentToSave: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentUpdate(text){
|
||||||
|
this.props.needSaving(true);
|
||||||
|
this.setState({contentToSave: text})
|
||||||
|
}
|
||||||
|
|
||||||
|
save(){
|
||||||
|
let file, blob = new window.Blob([this.state.contentToSave], {type : 'text/plain'});
|
||||||
|
try{
|
||||||
|
file = new window.File([blob], 'test.txt');
|
||||||
|
}catch(err){
|
||||||
|
// for crappy browser:
|
||||||
|
// https://stackoverflow.com/questions/33821631/alternative-for-file-constructor-for-safari
|
||||||
|
file = blob;
|
||||||
|
}
|
||||||
|
this.props.onSave(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div style={{height: '100%'}}>
|
||||||
|
<Editor onSave={this.save.bind(this)} filename={this.props.filename} content={this.props.content} onChange={this.onContentUpdate.bind(this)} height={this.state.height}/>
|
||||||
|
<NgIf cond={!this.props.isSaving}>
|
||||||
|
<Fab onClick={this.save.bind(this)}><Icon name="save" style={{height: '100%', width: '100%'}}/></Fab>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.props.isSaving}>
|
||||||
|
<Fab><Icon name="loading_white" style={{height: '100%', width: '100%'}}/></Fab>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ImageViewer = (props) => {
|
||||||
|
return (
|
||||||
|
<div style={{height: '100%'}}>
|
||||||
|
<MenuBar title={props.filename} download={props.data} />
|
||||||
|
<div style={{textAlign: 'center', background: '#525659', height: 'calc(100% - 34px)', overflow: 'hidden', padding: '20px', boxSizing: 'border-box'}}>
|
||||||
|
<img src={props.data} style={{maxHeight: '100%', maxWidth: '100%', minHeight: '100px', background: '#f1f1f1', boxShadow: theme.effects.shadow}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const PDFViewer = (props) => {
|
||||||
|
return (
|
||||||
|
<div style={{textAlign: 'center', background: '#525659', height: '100%'}}>
|
||||||
|
<embed src={props.data} type="application/pdf" style={{height: '100%', width: '100%'}}></embed>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioPlayer extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
wafesurfer: null,
|
||||||
|
loading: true,
|
||||||
|
isPlaying: true,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
this.toggle = this.toggle.bind(this);
|
||||||
|
window.addEventListener('keypress', this.toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
let $el = document.querySelector('.player');
|
||||||
|
var wavesurfer = WaveSurfer.create({
|
||||||
|
container: '#waveform',
|
||||||
|
waveColor: '#323639',
|
||||||
|
progressColor: '#6f6f6f',
|
||||||
|
cursorColor: '#323639',
|
||||||
|
cursorWidth: 2,
|
||||||
|
height: 250,
|
||||||
|
barWidth: 1
|
||||||
|
});
|
||||||
|
this.setState({wavesurfer: wavesurfer});
|
||||||
|
wavesurfer.on('ready', () => {
|
||||||
|
this.setState({loading: false});
|
||||||
|
if(this.state.isPlaying === true){
|
||||||
|
wavesurfer.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wavesurfer.on('error', (err) => {
|
||||||
|
this.setState({error: err, loading: false});
|
||||||
|
});
|
||||||
|
wavesurfer.load(this.props.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
if(this.state.wavesurfer){
|
||||||
|
this.state.wavesurfer.destroy();
|
||||||
|
}
|
||||||
|
window.removeEventListener('keypress', this.toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(){
|
||||||
|
if(this.state.isPlaying === true){
|
||||||
|
this.setState({isPlaying: false});
|
||||||
|
this.state.wavesurfer.pause();
|
||||||
|
}else{
|
||||||
|
this.setState({isPlaying: true});
|
||||||
|
this.state.wavesurfer.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPlay(e){
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({isPlaying: true})
|
||||||
|
this.state.wavesurfer.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPause(e){
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({isPlaying: false});
|
||||||
|
this.state.wavesurfer.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div style={{height: '100%'}}>
|
||||||
|
<MenuBar title={this.props.filename} download={this.props.data} />
|
||||||
|
<div style={{textAlign: 'center', background: '#525659', height: '100%', overflow: 'hidden', padding: '20px', boxSizing: 'border-box'}}>
|
||||||
|
<NgIf cond={this.state.error !== null} style={{color: 'white', marginTop: '30px'}}>
|
||||||
|
{this.state.error}
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.error === null}>
|
||||||
|
<NgIf cond={this.state.loading === true}>
|
||||||
|
<Icon name="loading" />
|
||||||
|
</NgIf>
|
||||||
|
<div style={{background: '#f1f1f1', boxShadow: theme.effects.shadow, opacity: this.state.loading? '0' : '1', position: 'relative'}}>
|
||||||
|
<div style={{position: 'absolute', top: '10px', right: '10px', zIndex: '2', height: '30px'}}>
|
||||||
|
<NgIf cond={this.state.isPlaying === false} style={{display: 'inline'}}>
|
||||||
|
<span style={{cursor: 'pointer'}} onClick={this.onPlay.bind(this)}><Icon name="play"/></span>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.isPlaying === true} style={{display: 'inline'}}>
|
||||||
|
<span style={{cursor: 'pointer'}} onClick={this.onPause.bind(this)}><Icon name="pause"/></span>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
<div id="waveform"></div>
|
||||||
|
</div>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoPlayer extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
this.player = videojs(this.videoNode, {
|
||||||
|
//autoplay: true,
|
||||||
|
controls: true,
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
fluid: false,
|
||||||
|
sources: [{
|
||||||
|
src: this.props.data
|
||||||
|
}]
|
||||||
|
}, function onPlayerReady() {
|
||||||
|
//console.log('onPlayerReady', this)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.player) {
|
||||||
|
this.player.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div style={{background: '#525659', height: '100%'}}>
|
||||||
|
<MenuBar title={this.props.filename} download={this.props.data} />
|
||||||
|
<div style={{padding: '20px'}}>
|
||||||
|
<div style={{maxWidth: '800px', width: '100%', margin: '0 auto'}}>
|
||||||
|
<video ref={ node => this.videoNode = node } className="video-js my-skin" style={{boxShadow: theme.effects.shadow}}></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuBar extends React.Component{
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {loading: false, id: null}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadRequest(){
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
id: window.setInterval(function(){
|
||||||
|
if(document.cookie){
|
||||||
|
this.setState({loading: false})
|
||||||
|
window.clearInterval(this.state.id);
|
||||||
|
}
|
||||||
|
}.bind(this), 80)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
window.clearInterval(this.state.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div style={{background: '#313538', color: '#f1f1f1', boxShadow: theme.effects.shadow_small}}>
|
||||||
|
<Container style={{padding: '9px 0', textAlign: 'center', color: '#f1f1f1', fontSize: '0.9em'}}>
|
||||||
|
<NgIf cond={this.props.hasOwnProperty('download')} style={{float: 'right', height: '1em'}}>
|
||||||
|
<NgIf cond={!this.state.loading} style={{display: 'inline'}}>
|
||||||
|
<a href={this.props.download} download={this.props.title} onClick={this.onDownloadRequest.bind(this)}>
|
||||||
|
<Icon name="download" style={{width: '15px', height: '15px'}} />
|
||||||
|
</a>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={this.state.loading} style={{display: 'inline'}}>
|
||||||
|
<Icon name="loading" style={{width: '15px', height: '15px'}} />
|
||||||
|
</NgIf>
|
||||||
|
</NgIf>
|
||||||
|
<span style={{letterSpacing: '0.3px'}}>{this.props.title}</span>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileDownloader extends React.Component{
|
||||||
|
constructor(props){
|
||||||
|
super(props)
|
||||||
|
this.state = {loading: false, id: null};
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(){
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
id: window.setInterval(function(){
|
||||||
|
if(document.cookie){
|
||||||
|
this.setState({loading: false})
|
||||||
|
window.clearInterval(this.state.id);
|
||||||
|
}
|
||||||
|
}.bind(this), 80)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
window.clearInterval(this.state.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div style={{textAlign: 'center', background: '#525659', height: '100%'}}>
|
||||||
|
<div style={{padding: '15px 20px', background: '#323639', borderRadius: '2px', color: 'inherit', boxShadow: theme.effects.shadow, display: 'inline-block', marginTop: '50px'}}>
|
||||||
|
<a download={this.props.filename} href={this.props.data}>
|
||||||
|
<NgIf onClick={this.onClick.bind(this)} cond={!this.state.loading} style={{fontSize: '17px', display: 'inline-block'}}>
|
||||||
|
DOWNLOAD
|
||||||
|
</NgIf>
|
||||||
|
</a>
|
||||||
|
<NgIf cond={this.state.loading} style={{height: '20px', display: 'inline-block'}}>
|
||||||
|
<Icon name="loading"/>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/pages/viewerpage/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
test: 'test'
|
||||||
|
}
|
||||||
|
|
||||||
10
src/pages/viewerpage/javascript.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// export function test(){
|
||||||
|
// return 'aaaa';
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
function cube(x) {
|
||||||
|
return x * x * x;
|
||||||
|
}
|
||||||
|
const foo = Math.PI + Math.SQRT2;
|
||||||
|
export { cube, foo };
|
||||||
24
src/router.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { BrowserRouter, Route, IndexRoute } from 'react-router-dom'
|
||||||
|
import { NotFoundPage, ConnectPage, HomePage, FilesPage, ViewerPage, LogoutPage } from './pages/';
|
||||||
|
import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './utilities/';
|
||||||
|
import { createBrowserHistory } from 'history'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default class AppRouter extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter history={createBrowserHistory()}>
|
||||||
|
<div>
|
||||||
|
<Route exact path="/" component={HomePage} />
|
||||||
|
<Route path="/login" component={ConnectPage} />
|
||||||
|
<Route path="/files/:path*" component={FilesPage} />
|
||||||
|
<Route path="/view/:path*" component={ViewerPage} />
|
||||||
|
<Route path="/logout" component={LogoutPage} />
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
src/utilities/backpressure.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
export function debounce(func, wait, immediate) {
|
||||||
|
var timeout;
|
||||||
|
return function() {
|
||||||
|
var context = this, args = arguments;
|
||||||
|
var later = function() {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) func.apply(context, args);
|
||||||
|
};
|
||||||
|
var callNow = immediate && !timeout;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) func.apply(context, args);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function throttle(func, wait, options) {
|
||||||
|
var context, args, result;
|
||||||
|
var timeout = null;
|
||||||
|
var previous = 0;
|
||||||
|
if (!options) options = {};
|
||||||
|
var later = function() {
|
||||||
|
previous = options.leading === false ? 0 : Date.now();
|
||||||
|
timeout = null;
|
||||||
|
result = func.apply(context, args);
|
||||||
|
if (!timeout) context = args = null;
|
||||||
|
};
|
||||||
|
return function() {
|
||||||
|
var now = Date.now();
|
||||||
|
if (!previous && options.leading === false) previous = now;
|
||||||
|
var remaining = wait - (now - previous);
|
||||||
|
context = this;
|
||||||
|
args = arguments;
|
||||||
|
if (remaining <= 0 || remaining > wait) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = null;
|
||||||
|
}
|
||||||
|
previous = now;
|
||||||
|
result = func.apply(context, args);
|
||||||
|
if (!timeout) context = args = null;
|
||||||
|
} else if (!timeout && options.trailing !== false) {
|
||||||
|
timeout = setTimeout(later, remaining);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
||||||
38
src/utilities/button.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
export class Button extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
style(){
|
||||||
|
let style = {};
|
||||||
|
style.border = 'none';
|
||||||
|
style.margin = '0';
|
||||||
|
style.padding = '5px';
|
||||||
|
style.width = '100%';
|
||||||
|
style.display = 'inline-block';
|
||||||
|
style.outline = 'none';
|
||||||
|
style.cursor = 'pointer';
|
||||||
|
style.fontSize = 'inherit';
|
||||||
|
style.borderRadius = '2px';
|
||||||
|
style.color = 'inherit';
|
||||||
|
if(this.props.theme === 'primary'){ style.background = theme.colors.primary; style.color = 'white'}
|
||||||
|
else if(this.props.theme === 'secondary'){ style.background = theme.colors.secondary; style.color = 'white'}
|
||||||
|
else if(this.props.theme === 'emphasis'){ style.background = theme.colors.emphasis; style.color = 'white'}
|
||||||
|
else{style.background = 'inherit'}
|
||||||
|
return Object.assign(style, this.props.style);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<button onClick={this.props.onClick} style={this.style()}>{this.props.children}</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.propTypes = {
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
49
src/utilities/card.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {theme} from './theme';
|
||||||
|
|
||||||
|
export class Card extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
dragging: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(){
|
||||||
|
if(this.props.onClick){
|
||||||
|
this.props.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter(){
|
||||||
|
if(this.props.onMouseEnter){
|
||||||
|
this.props.onMouseEnter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave(){
|
||||||
|
if(this.props.onMouseLeave){
|
||||||
|
this.props.onMouseLeave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let style = {};
|
||||||
|
style.padding = '10px';
|
||||||
|
style.color = '#474747';
|
||||||
|
style.cursor = 'pointer';
|
||||||
|
style.margin = '2px 0';
|
||||||
|
style.background = 'white'; style.boxShadow = theme.effects.shadow_large;
|
||||||
|
style.overflow = 'hidden';
|
||||||
|
style.position = 'relative';
|
||||||
|
for(let key in this.props.style){
|
||||||
|
style[key] = this.props.style[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onClick={this.onClick.bind(this)} onMouseEnter={this.onMouseEnter.bind(this)} onMouseLeave={this.onMouseLeave.bind(this)} style={style}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/utilities/container.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export class Container extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const style = Object.assign({width: '95%', maxWidth: this.props.maxWidth || '800px', margin: '0 auto', padding: '10px'}, this.props.style || {});
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/utilities/crypto.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
|
const algorithm = 'aes-256-ctr';
|
||||||
|
|
||||||
|
export function encrypt(obj, key){
|
||||||
|
const cipher = crypto.createCipher(algorithm, key);
|
||||||
|
return cipher.update(JSON.stringify(obj), 'utf8', 'hex') + cipher.final('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function decrypt(text, key){
|
||||||
|
var decipher = crypto.createDecipher(algorithm, key)
|
||||||
|
try{
|
||||||
|
return JSON.parse(decipher.update(text,'hex','utf8') + decipher.final('utf8'));
|
||||||
|
}catch(err){
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/utilities/error.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { NgIf } from './';
|
||||||
|
|
||||||
|
export const Error = (props) => {
|
||||||
|
let style = props.style || {};
|
||||||
|
style.textAlign = 'center';
|
||||||
|
style.marginTop = '50px';
|
||||||
|
style.fontSize = '25px';
|
||||||
|
style.fontStyle = 'italic';
|
||||||
|
style.fontWeight = 100;
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
{props.err.message || "Oups something went wrong :/"}
|
||||||
|
<NgIf cond={props.err.trace !== undefined} style={{fontSize: '12px', maxWidth: '500px', margin: '10px auto 0 auto'}}>
|
||||||
|
{JSON.stringify(props.err.trace)}
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/utilities/fab.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
export const Fab = (props) => {
|
||||||
|
let style = {};
|
||||||
|
style.height = '25px';
|
||||||
|
style.width = '25px';
|
||||||
|
style.padding = '13px';
|
||||||
|
style.position = 'fixed';
|
||||||
|
style.bottom = '20px';
|
||||||
|
style.right = '20px';
|
||||||
|
style.borderRadius = '50%';
|
||||||
|
style.background = theme.colors.text;
|
||||||
|
style.boxShadow = theme.effects.shadow;
|
||||||
|
style.zIndex = '1000';
|
||||||
|
style.cursor = 'pointer';
|
||||||
|
return (
|
||||||
|
<div onClick={props.onClick} style={style}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
44
src/utilities/icon.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const Icon = (props) => {
|
||||||
|
let url;
|
||||||
|
if(props.name === 'directory'){
|
||||||
|
url = "/img/folder.svg";
|
||||||
|
}else if(props.name === 'file'){
|
||||||
|
url = "/img/file.svg";
|
||||||
|
}else if(props.name === 'loader'){
|
||||||
|
url = "/img/loader.svg";
|
||||||
|
}else if(props.name === 'save'){
|
||||||
|
url = "/img/save.svg";
|
||||||
|
}else if(props.name === 'power'){
|
||||||
|
url = "/img/power.svg";
|
||||||
|
}else if(props.name === 'edit'){
|
||||||
|
url = "/img/edit.svg";
|
||||||
|
}else if(props.name === 'delete'){
|
||||||
|
url = "/img/delete.svg";
|
||||||
|
}else if(props.name === 'bucket'){
|
||||||
|
url = "/img/bucket.svg";
|
||||||
|
}else if(props.name === 'link'){
|
||||||
|
url = "/img/link.svg";
|
||||||
|
}else if(props.name === 'loading'){
|
||||||
|
url = "/img/loader.svg";
|
||||||
|
}else if(props.name === 'download'){
|
||||||
|
url = "/img/download.svg";
|
||||||
|
}else if(props.name === 'play'){
|
||||||
|
url = "/img/play.svg";
|
||||||
|
}else if(props.name === 'pause'){
|
||||||
|
url = "/img/pause.svg";
|
||||||
|
}else if(props.name === 'error'){
|
||||||
|
url = "/img/error.svg";
|
||||||
|
}else if(props.name === 'loading_white'){
|
||||||
|
url = "/img/loader_white.svg";
|
||||||
|
}else{
|
||||||
|
throw('unknown icon');
|
||||||
|
}
|
||||||
|
let style = props.style || {};
|
||||||
|
style.verticalAlign = 'bottom';
|
||||||
|
style.maxHeight = '100%';
|
||||||
|
return (
|
||||||
|
<img onClick={props.onClick} src={url} alt={props.name} style={style}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/utilities/index.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export { Input } from './input';
|
||||||
|
export { Textarea } from './textarea';
|
||||||
|
export { Button } from './button';
|
||||||
|
export { Container } from './container';
|
||||||
|
export { LoremIpsum } from './loremipsum';
|
||||||
|
export { NgIf } from './ngif';
|
||||||
|
export { Card } from './card';
|
||||||
|
export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate';
|
||||||
|
export { debounce, throttle } from './backpressure';
|
||||||
|
export { Loader } from './loader';
|
||||||
|
export { Error } from './error';
|
||||||
|
export { Fab } from './fab';
|
||||||
|
export { Icon } from './icon';
|
||||||
|
export { Notification } from './notification';
|
||||||
|
export { Uploader } from './uploader';
|
||||||
|
export { theme } from './theme';
|
||||||
|
export { encrypt, decrypt } from './crypto'
|
||||||
|
export { pathBuilder } from './path';
|
||||||
44
src/utilities/input.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
export class Input extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
style(){
|
||||||
|
let style = this.props.style || {};
|
||||||
|
style.background = 'inherit';
|
||||||
|
style.border = 'none';
|
||||||
|
style.borderRadius = '0';
|
||||||
|
style.borderBottom = '2px solid rgba(70, 99, 114, 0.1)'
|
||||||
|
style.width = '100%';
|
||||||
|
style.display = 'inline-block';
|
||||||
|
style.fontSize = 'inherit';
|
||||||
|
style.padding = '5px 0px 5px 0px';
|
||||||
|
style.margin = '0 0 8px 0';
|
||||||
|
style.outline = 'none';
|
||||||
|
style.boxSizing = 'border-box';
|
||||||
|
style.color = 'inherit';
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
style={this.style()}
|
||||||
|
name={this.props.name}
|
||||||
|
type={this.props.type}
|
||||||
|
value={this.props.value}
|
||||||
|
defaultValue={this.props.defaultValue}
|
||||||
|
placeholder={this.props.placeholder || ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Input.propTypes = {
|
||||||
|
type: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string
|
||||||
|
};
|
||||||
19
src/utilities/loader.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const Loader = (props) => {
|
||||||
|
let style = props.style || {};
|
||||||
|
style.textAlign = 'center';
|
||||||
|
style.marginTop = '50px';
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<svg width="120px" height="120px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||||
|
<rect x="0" y="0" width="100" height="100" fill="none"></rect>
|
||||||
|
<circle cx="50" cy="50" r="40" stroke="rgba(100%,100%,100%,0.679)" fill="none" strokeWidth="10" strokeLinecap="round"></circle>
|
||||||
|
<circle cx="50" cy="50" r="40" stroke="#6f6f6f" fill="none" strokeWidth="6" strokeLinecap="round">
|
||||||
|
<animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite" from="0" to="502"></animate>
|
||||||
|
<animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/utilities/loremipsum
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export class LoremIpsum extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/utilities/navigate.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
//import { history } from '../history';
|
||||||
|
|
||||||
|
export const URL_HOME = '/';
|
||||||
|
export function goToHome(history){
|
||||||
|
history.push(URL_HOME);
|
||||||
|
return Promise.resolve('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const URL_FILES = '/files';
|
||||||
|
export function goToFiles(history, path, state){
|
||||||
|
history.push(URL_FILES+"?path="+encode_path(path), state);
|
||||||
|
return Promise.resolve('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const URL_VIEWER = '/view';
|
||||||
|
export function goToViewer(history, path, state){
|
||||||
|
history.push(URL_VIEWER+'?path='+encode_path(path), state);
|
||||||
|
return Promise.resolve('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const URL_LOGIN = '/login';
|
||||||
|
export function goToLogin(history){
|
||||||
|
history.push(URL_EDIT);
|
||||||
|
return Promise.resolve('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const URL_LOGOUT = '/logout';
|
||||||
|
export function goToLogout(history){
|
||||||
|
history.push(URL_LOGOUT);
|
||||||
|
return Promise.resolve('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode_path(path){
|
||||||
|
if(/%2F/.test(path) === false){
|
||||||
|
return encodeURIComponent(path).replace(/%2F/g, '/'); // replace slash to make url more friendly
|
||||||
|
}else{
|
||||||
|
return encodeURIComponent(path) // in case you got a %2F folder somewhere ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepare(path){
|
||||||
|
return encodeURIComponent(decodeURIComponent(path)); // to send our url correctly without using directly '/'
|
||||||
|
}
|
||||||
20
src/utilities/ngif.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export class NgIf extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if(this.props.cond){
|
||||||
|
return <div onClick={this.props.onClick} style={this.props.style}>{this.props.children}</div>;
|
||||||
|
}else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NgIf.propTypes = {
|
||||||
|
cond: PropTypes.bool
|
||||||
|
};
|
||||||
66
src/utilities/notification.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { NgIf } from './';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
export class Notification extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
visible: null,
|
||||||
|
error: null,
|
||||||
|
timeout: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount(){
|
||||||
|
this.componentWillReceiveProps(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
window.clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(props){
|
||||||
|
if(props.error !== null){
|
||||||
|
this.componentWillUnmount();
|
||||||
|
this.setState({visible: true, error: props.error});
|
||||||
|
this.timeout = window.setTimeout(() => {
|
||||||
|
this.setState({visible: null});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleVisibility(){
|
||||||
|
this.setState({visible: !this.state.visible});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatError(err){
|
||||||
|
if(typeof err === 'object'){
|
||||||
|
if(err && err.message){
|
||||||
|
return err.message
|
||||||
|
}else{
|
||||||
|
return JSON.stringify(err);
|
||||||
|
}
|
||||||
|
}else if(typeof err === 'string'){
|
||||||
|
return err;
|
||||||
|
}else{
|
||||||
|
throw('unrecognized notification')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<NgIf cond={this.state.visible === true} style={{position: 'fixed', bottom: 0, left: 0, right: 0, textAlign: 'center'}}>
|
||||||
|
<div onClick={this.toggleVisibility.bind(this)} style={{display: 'inline-block', background: '#637d8b', minWidth: '200px', maxWidth: '400px', margin: '0 auto', padding: '10px 15px', borderTopLeftRadius: '3px', borderTopRightRadius: '3px', color: 'white', textAlign: 'left', cursor: 'pointer', boxShadow: theme.effects.shadow}}>
|
||||||
|
{this.formatError(this.state.error)}
|
||||||
|
</div>
|
||||||
|
</NgIf>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification.propTypes = {
|
||||||
|
error: PropTypes.any
|
||||||
|
}
|
||||||
10
src/utilities/path.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Path from 'path';
|
||||||
|
|
||||||
|
export function pathBuilder(path, filename, type = 'file'){
|
||||||
|
let tmp = Path.resolve(path, filename)
|
||||||
|
if(type === 'file'){
|
||||||
|
return tmp;
|
||||||
|
}else{
|
||||||
|
return tmp + '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/utilities/textarea.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
export class Textarea extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
style(){
|
||||||
|
let style = this.props.style || {};
|
||||||
|
style.background = 'inherit';
|
||||||
|
style.border = 'none';
|
||||||
|
style.borderRadius = '0';
|
||||||
|
style.borderBottom = '2px solid rgba(70, 99, 114, 0.1)'
|
||||||
|
style.width = '100%';
|
||||||
|
style.display = 'inline-block';
|
||||||
|
style.fontSize = 'inherit';
|
||||||
|
style.padding = '5px 0px 5px 0px';
|
||||||
|
style.margin = '0 0 8px 0';
|
||||||
|
style.outline = 'none';
|
||||||
|
style.boxSizing = 'border-box';
|
||||||
|
style.color = 'inherit';
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
style={this.style()}
|
||||||
|
name={this.props.name}
|
||||||
|
type={this.props.type}
|
||||||
|
value={this.props.value}
|
||||||
|
defaultValue={this.props.defaultValue}
|
||||||
|
placeholder={this.props.placeholder || ''}
|
||||||
|
></textarea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Textarea.propTypes = {
|
||||||
|
type: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string
|
||||||
|
};
|
||||||
18
src/utilities/theme.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const theme = {
|
||||||
|
colors: {
|
||||||
|
primary: '#f89e6b',
|
||||||
|
secondary: '#466372',
|
||||||
|
emphasis: '#375160',
|
||||||
|
error: '#ff0000',
|
||||||
|
text: '#6f6f6f'
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
normal: '10px',
|
||||||
|
big: '20px'
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
shadow_small: 'rgba(0, 0, 0, 0.14) 2px 2px 2px 0px',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px',
|
||||||
|
shadow_large: 'rgba(158, 163, 172, 0.3) 0px 19px 60px, rgba(158, 163, 172, 0.22) 0px 15px 20px'
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/utilities/uploader.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { DropTarget, DragSource } from 'react-dnd';
|
||||||
|
import { NgIf } from './';
|
||||||
|
|
||||||
|
const FileTarget = {
|
||||||
|
drop(props, monitor) {
|
||||||
|
props.onUpload(props.path, monitor.getItem().files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DropTarget('__NATIVE_FILE__', FileTarget, (connect, monitor) => ({
|
||||||
|
connectDropTarget: connect.dropTarget(),
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
canDrop: monitor.canDrop()
|
||||||
|
}))
|
||||||
|
export class Uploader extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
drop: false,
|
||||||
|
dragging: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
const style = {
|
||||||
|
position: 'absolute', top: 0, bottom: 0, left: 0, right: 0,
|
||||||
|
background: 'rgba(0,0,0,0.2)',
|
||||||
|
padding: '50% 0',
|
||||||
|
textAlign: 'center'
|
||||||
|
}
|
||||||
|
return this.props.connectDropTarget(
|
||||||
|
<div>
|
||||||
|
<NgIf cond={this.props.isOver && this.props.canDrop} style={style}>
|
||||||
|
DRAG FILE HERE
|
||||||
|
</NgIf>
|
||||||
|
<div>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Uploader.PropTypes = {
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
onUpload: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
65
webpack.config.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const path = require('path');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
entry: [
|
||||||
|
'babel-polyfill',
|
||||||
|
path.join(__dirname, 'src', 'client.js')
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'server', 'public'),
|
||||||
|
publicPath: '/',
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: path.join(__dirname, 'src'),
|
||||||
|
loader: ['babel-loader']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.html$/,
|
||||||
|
loader: 'html-loader'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
|
||||||
|
}),
|
||||||
|
new webpack.optimize.OccurrenceOrderPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: __dirname + '/src/index.html',
|
||||||
|
inject:true
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(process.env.NODE_ENV === 'production'){
|
||||||
|
config.plugins.push(new webpack.optimize.UglifyJsPlugin());
|
||||||
|
}else{
|
||||||
|
config.devtool = '#inline-source-map';
|
||||||
|
config.devServer = {
|
||||||
|
contentBase: path.join(__dirname, "server", "public"),
|
||||||
|
disableHostCheck: true,
|
||||||
|
hot: true,
|
||||||
|
historyApiFallback: {
|
||||||
|
index: 'index.html'
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:3000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
config.entry.push('webpack/hot/only-dev-server');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||