From e9f8f7a9bd6616188a5796ae35cee85cdbab4655 Mon Sep 17 00:00:00 2001 From: Mickael KERJEAN Date: Tue, 13 Jun 2017 17:44:33 +1000 Subject: [PATCH] code base cleanup --- .babelrc | 4 + .gitignore | 6 + README.org | 50 + config.js | 23 + docker/docker-compose.yml | 11 + docker/img/Dockerfile | 13 + package.json | 72 ++ server/bootstrap.js | 32 + server/ctrl/files.js | 135 ++ server/ctrl/session.js | 54 + server/index.js | 17 + server/model/backend/dropbox.js | 146 +++ server/model/backend/ftp.js | 161 +++ server/model/backend/gdrive.js | 486 ++++++++ server/model/backend/s3.js | 241 ++++ server/model/backend/sftp.js | 89 ++ server/model/backend/webdav.js | 123 ++ server/model/files.js | 99 ++ server/model/session.js | 39 + server/public/css/codemirror.css | 378 ++++++ server/public/css/style.css | 48 + server/public/css/video-js.css | 1309 ++++++++++++++++++++ server/public/css/videojs-sublime-skin.css | 280 +++++ server/public/img/bucket.svg | 6 + server/public/img/delete.svg | 43 + server/public/img/download.svg | 10 + server/public/img/dropbox.png | Bin 0 -> 9130 bytes server/public/img/edit.svg | 9 + server/public/img/error.svg | 37 + server/public/img/file.svg | 4 + server/public/img/folder copy.svg | 6 + server/public/img/folder.svg | 6 + server/public/img/google-drive.png | Bin 0 -> 10710 bytes server/public/img/link.svg | 6 + server/public/img/loader.svg | 1 + server/public/img/loader_white.svg | 1 + server/public/img/logo.png | Bin 0 -> 9525 bytes server/public/img/logo_large.png | Bin 0 -> 7007 bytes server/public/img/pause.svg | 38 + server/public/img/play.svg | 37 + server/public/img/power.svg | 39 + server/public/img/save.svg | 38 + server/public/index.html | 1 + server/utils/crypto.js | 26 + server/utils/mimetype.js | 217 ++++ src/client.js | 8 + src/components/api.js | 1 + src/components/breadcrumb.js | 169 +++ src/components/connect.js | 15 + src/components/editor.js | 168 +++ src/components/filesystem.js | 121 ++ src/components/index.js | 4 + src/data/api.js | 107 ++ src/data/events.js | 78 ++ src/data/index.js | 6 + src/data/mimetype.js | 218 ++++ src/data/password.js | 14 + src/data/tools.js | 140 +++ src/index.html | 22 + src/pages/connectpage.js | 299 +++++ src/pages/filespage.js | 211 ++++ src/pages/filespage/breadcrumb.js | 69 ++ src/pages/filespage/existingthing.js | 306 +++++ src/pages/filespage/filezone.js | 49 + src/pages/filespage/index.js | 3 + src/pages/filespage/newthing.js | 84 ++ src/pages/homepage.js | 28 + src/pages/index.js | 6 + src/pages/logout.js | 25 + src/pages/notfound.js | 16 + src/pages/viewerpage.js | 423 +++++++ src/pages/viewerpage/index.js | 4 + src/pages/viewerpage/javascript.js | 10 + src/router.js | 24 + src/utilities/backpressure.js | 48 + src/utilities/button.js | 38 + src/utilities/card.js | 49 + src/utilities/container.js | 16 + src/utilities/crypto.js | 17 + src/utilities/error.js | 19 + src/utilities/fab.js | 22 + src/utilities/icon.js | 44 + src/utilities/index.js | 18 + src/utilities/input.js | 44 + src/utilities/loader.js | 19 + src/utilities/loremipsum | 16 + src/utilities/navigate.js | 44 + src/utilities/ngif.js | 20 + src/utilities/notification.js | 66 + src/utilities/path.js | 10 + src/utilities/textarea.js | 44 + src/utilities/theme.js | 18 + src/utilities/uploader.js | 48 + webpack.config.js | 65 + 94 files changed, 7634 insertions(+) create mode 100644 .babelrc create mode 100644 .gitignore create mode 100644 README.org create mode 100644 config.js create mode 100644 docker/docker-compose.yml create mode 100644 docker/img/Dockerfile create mode 100644 package.json create mode 100644 server/bootstrap.js create mode 100644 server/ctrl/files.js create mode 100644 server/ctrl/session.js create mode 100644 server/index.js create mode 100644 server/model/backend/dropbox.js create mode 100644 server/model/backend/ftp.js create mode 100644 server/model/backend/gdrive.js create mode 100644 server/model/backend/s3.js create mode 100644 server/model/backend/sftp.js create mode 100644 server/model/backend/webdav.js create mode 100644 server/model/files.js create mode 100644 server/model/session.js create mode 100644 server/public/css/codemirror.css create mode 100644 server/public/css/style.css create mode 100644 server/public/css/video-js.css create mode 100644 server/public/css/videojs-sublime-skin.css create mode 100644 server/public/img/bucket.svg create mode 100644 server/public/img/delete.svg create mode 100644 server/public/img/download.svg create mode 100644 server/public/img/dropbox.png create mode 100644 server/public/img/edit.svg create mode 100644 server/public/img/error.svg create mode 100644 server/public/img/file.svg create mode 100644 server/public/img/folder copy.svg create mode 100644 server/public/img/folder.svg create mode 100644 server/public/img/google-drive.png create mode 100644 server/public/img/link.svg create mode 100644 server/public/img/loader.svg create mode 100644 server/public/img/loader_white.svg create mode 100644 server/public/img/logo.png create mode 100644 server/public/img/logo_large.png create mode 100644 server/public/img/pause.svg create mode 100644 server/public/img/play.svg create mode 100644 server/public/img/power.svg create mode 100644 server/public/img/save.svg create mode 100644 server/public/index.html create mode 100644 server/utils/crypto.js create mode 100644 server/utils/mimetype.js create mode 100644 src/client.js create mode 100644 src/components/api.js create mode 100644 src/components/breadcrumb.js create mode 100644 src/components/connect.js create mode 100644 src/components/editor.js create mode 100644 src/components/filesystem.js create mode 100644 src/components/index.js create mode 100644 src/data/api.js create mode 100644 src/data/events.js create mode 100644 src/data/index.js create mode 100644 src/data/mimetype.js create mode 100644 src/data/password.js create mode 100644 src/data/tools.js create mode 100644 src/index.html create mode 100644 src/pages/connectpage.js create mode 100644 src/pages/filespage.js create mode 100644 src/pages/filespage/breadcrumb.js create mode 100644 src/pages/filespage/existingthing.js create mode 100644 src/pages/filespage/filezone.js create mode 100644 src/pages/filespage/index.js create mode 100644 src/pages/filespage/newthing.js create mode 100644 src/pages/homepage.js create mode 100644 src/pages/index.js create mode 100644 src/pages/logout.js create mode 100644 src/pages/notfound.js create mode 100644 src/pages/viewerpage.js create mode 100644 src/pages/viewerpage/index.js create mode 100644 src/pages/viewerpage/javascript.js create mode 100644 src/router.js create mode 100644 src/utilities/backpressure.js create mode 100644 src/utilities/button.js create mode 100644 src/utilities/card.js create mode 100644 src/utilities/container.js create mode 100644 src/utilities/crypto.js create mode 100644 src/utilities/error.js create mode 100644 src/utilities/fab.js create mode 100644 src/utilities/icon.js create mode 100644 src/utilities/index.js create mode 100644 src/utilities/input.js create mode 100644 src/utilities/loader.js create mode 100644 src/utilities/loremipsum create mode 100644 src/utilities/navigate.js create mode 100644 src/utilities/ngif.js create mode 100644 src/utilities/notification.js create mode 100644 src/utilities/path.js create mode 100644 src/utilities/textarea.js create mode 100644 src/utilities/theme.js create mode 100644 src/utilities/uploader.js create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..628173d2 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + 'presets': ['react', 'es2015', 'stage-2'], + 'plugins': ["transform-decorators-legacy", "syntax-dynamic-import"] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6286b5e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +server/public/bundle.js +node_modules/ +babel_cache/ +.DS_Store +\#.*\# +.\#.* \ No newline at end of file diff --git a/README.org b/README.org new file mode 100644 index 00000000..b110a08b --- /dev/null +++ b/README.org @@ -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 + +![edit interface](http://mickael.kerjean.free.fr/assets/images/screenshots/exploreur_0.png) +![list files](http://mickael.kerjean.free.fr/assets/images/screenshots/exploreur_0.png) + +* 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 diff --git a/config.js b/config.js new file mode 100644 index 00000000..e0f6575e --- /dev/null +++ b/config.js @@ -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" + } +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..8b6e5cc6 --- /dev/null +++ b/docker/docker-compose.yml @@ -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" \ No newline at end of file diff --git a/docker/img/Dockerfile b/docker/img/Dockerfile new file mode 100644 index 00000000..8e915612 --- /dev/null +++ b/docker/img/Dockerfile @@ -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"] \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..b84a8f96 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server/bootstrap.js b/server/bootstrap.js new file mode 100644 index 00000000..8b06fef1 --- /dev/null +++ b/server/bootstrap.js @@ -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; diff --git a/server/ctrl/files.js b/server/ctrl/files.js new file mode 100644 index 00000000..f30430ac --- /dev/null +++ b/server/ctrl/files.js @@ -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; diff --git a/server/ctrl/session.js b/server/ctrl/session.js new file mode 100644 index 00000000..098be6df --- /dev/null +++ b/server/ctrl/session.js @@ -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; diff --git a/server/index.js b/server/index.js new file mode 100644 index 00000000..f676798b --- /dev/null +++ b/server/index.js @@ -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"); } +}); diff --git a/server/model/backend/dropbox.js b/server/model/backend/dropbox.js new file mode 100644 index 00000000..f3386caa --- /dev/null +++ b/server/model/backend/dropbox.js @@ -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')); + } +} diff --git a/server/model/backend/ftp.js b/server/model/backend/ftp.js new file mode 100644 index 00000000..2cf067e5 --- /dev/null +++ b/server/model/backend/ftp.js @@ -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'); } + }); + }); + }); + } +} + + diff --git a/server/model/backend/gdrive.js b/server/model/backend/gdrive.js new file mode 100644 index 00000000..638804ac --- /dev/null +++ b/server/model/backend/gdrive.js @@ -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 { + if(cache.length === 0){ return true;} + else{ + for(let i=0, j=cache.length; i { + 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'); } + }) + }) + }) + }); + } +} diff --git a/server/model/backend/s3.js b/server/model/backend/s3.js new file mode 100644 index 00000000..797d2f48 --- /dev/null +++ b/server/model/backend/s3.js @@ -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') } + }); + }); + }) + } +} diff --git a/server/model/backend/sftp.js b/server/model/backend/sftp.js new file mode 100644 index 00000000..3b5f0854 --- /dev/null +++ b/server/model/backend/sftp.js @@ -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)) + } +} diff --git a/server/model/backend/webdav.js b/server/model/backend/webdav.js new file mode 100644 index 00000000..58bab269 --- /dev/null +++ b/server/model/backend/webdav.js @@ -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'); } + }); + }); + } +} diff --git a/server/model/files.js b/server/model/files.js new file mode 100644 index 00000000..f08b8184 --- /dev/null +++ b/server/model/files.js @@ -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); + }); +} diff --git a/server/model/session.js b/server/model/session.js new file mode 100644 index 00000000..ec84fca6 --- /dev/null +++ b/server/model/session.js @@ -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); + }); +} + diff --git a/server/public/css/codemirror.css b/server/public/css/codemirror.css new file mode 100644 index 00000000..0866a2c8 --- /dev/null +++ b/server/public/css/codemirror.css @@ -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; } diff --git a/server/public/css/style.css b/server/public/css/style.css new file mode 100644 index 00000000..74a1f173 --- /dev/null +++ b/server/public/css/style.css @@ -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%;} diff --git a/server/public/css/video-js.css b/server/public/css/video-js.css new file mode 100644 index 00000000..e4b2a419 --- /dev/null +++ b/server/public/css/video-js.css @@ -0,0 +1,1309 @@ +.video-js{outline: none;} + +.video-js .vjs-big-play-button:before, .video-js .vjs-control:before, .video-js .vjs-modal-dialog, .vjs-modal-dialog .vjs-modal-dialog-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + +.video-js .vjs-big-play-button:before, .video-js .vjs-control:before { + text-align: center; } + +@font-face { + font-family: VideoJS; + src: url("font/VideoJS.eot?#iefix") format("eot"); } + +@font-face { + font-family: VideoJS; + src: url(data:application/font-woff;charset=utf-8;base64,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) format("woff"), url(data:application/x-font-ttf;charset=utf-8;base64,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FjY3PgE3LgEnIwYfAR4BFw4BBTM/ARUzESMGAyUVJwMchM2UWwgNq4JHrQgBAapUaAoJcWMBfiIhDiMrAQJLMB0BBAokNAIBPmMiIQ4iLAECSzAeAQUKJDQBP2MiIQ4iLAECSzAeAQUKJDQBAT75g+5B4arNLNIBJ44ByQL9BQ9mvYCKwA8FBQMDwwJVTGdzBf6VB8IHNR08lld9uT4LCRA/qGNxvUwHNR08lld9uT4LCRA/qGNxvUwHNR08lld9uT4LCRA/qGNxvVJkAWUDDEf+tYP5AQAAAAEAAAAABiAGtgAbAAABBAADER4BFzMRITU2ADcWABcVIREzPgE3EQIAA4D+4v6FBwJ/X+D+1QYBJ97eAScG/tXgX38CB/6FBrUH/oX+4v32X38CAlWV3gEnBgb+2d6V/asCf18CCgEeAXsAAAAAEADGAAEAAAAAAAEABwAAAAEAAAAAAAIABwAHAAEAAAAAAAMABwAOAAEAAAAAAAQABwAVAAEAAAAAAAUACwAcAAEAAAAAAAYABwAnAAEAAAAAAAoAKwAuAAEAAAAAAAsAEwBZAAMAAQQJAAEADgBsAAMAAQQJAAIADgB6AAMAAQQJAAMADgCIAAMAAQQJAAQADgCWAAMAAQQJAAUAFgCkAAMAAQQJAAYADgC6AAMAAQQJAAoAVgDIAAMAAQQJAAsAJgEeVmlkZW9KU1JlZ3VsYXJWaWRlb0pTVmlkZW9KU1ZlcnNpb24gMS4wVmlkZW9KU0dlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAFYAaQBkAGUAbwBKAFMAUgBlAGcAdQBsAGEAcgBWAGkAZABlAG8ASgBTAFYAaQBkAGUAbwBKAFMAVgBlAHIAcwBpAG8AbgAgADEALgAwAFYAaQBkAGUAbwBKAFMARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAgAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAABAgEDAQQBBQEGAQcBCAEJAQoBCwEMAQ0BDgEPARABEQESARMBFAEVARYBFwEYARkBGgEbARwBHQEeAR8EcGxheQtwbGF5LWNpcmNsZQVwYXVzZQt2b2x1bWUtbXV0ZQp2b2x1bWUtbG93CnZvbHVtZS1taWQLdm9sdW1lLWhpZ2gQZnVsbHNjcmVlbi1lbnRlcg9mdWxsc2NyZWVuLWV4aXQGc3F1YXJlB3NwaW5uZXIJc3VidGl0bGVzCGNhcHRpb25zCGNoYXB0ZXJzBXNoYXJlA2NvZwZjaXJjbGUOY2lyY2xlLW91dGxpbmUTY2lyY2xlLWlubmVyLWNpcmNsZQJoZAZjYW5jZWwGcmVwbGF5CGZhY2Vib29rBWdwbHVzCGxpbmtlZGluB3R3aXR0ZXIGdHVtYmxyCXBpbnRlcmVzdBFhdWRpby1kZXNjcmlwdGlvbgVhdWRpbwAAAAAA) format("truetype"); + font-weight: normal; + font-style: normal; } + +.vjs-icon-play, .video-js .vjs-big-play-button, .video-js .vjs-play-control { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-play:before, .video-js .vjs-big-play-button:before, .video-js .vjs-play-control:before { + content: "\f101"; } + +.vjs-icon-play-circle { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-play-circle:before { + content: "\f102"; } + +.vjs-icon-pause, .video-js .vjs-play-control.vjs-playing { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-pause:before, .video-js .vjs-play-control.vjs-playing:before { + content: "\f103"; } + +.vjs-icon-volume-mute, .video-js .vjs-mute-control.vjs-vol-0, +.video-js .vjs-volume-menu-button.vjs-vol-0 { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-mute:before, .video-js .vjs-mute-control.vjs-vol-0:before, + .video-js .vjs-volume-menu-button.vjs-vol-0:before { + content: "\f104"; } + +.vjs-icon-volume-low, .video-js .vjs-mute-control.vjs-vol-1, +.video-js .vjs-volume-menu-button.vjs-vol-1 { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-low:before, .video-js .vjs-mute-control.vjs-vol-1:before, + .video-js .vjs-volume-menu-button.vjs-vol-1:before { + content: "\f105"; } + +.vjs-icon-volume-mid, .video-js .vjs-mute-control.vjs-vol-2, +.video-js .vjs-volume-menu-button.vjs-vol-2 { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-mid:before, .video-js .vjs-mute-control.vjs-vol-2:before, + .video-js .vjs-volume-menu-button.vjs-vol-2:before { + content: "\f106"; } + +.vjs-icon-volume-high, .video-js .vjs-mute-control, +.video-js .vjs-volume-menu-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-high:before, .video-js .vjs-mute-control:before, + .video-js .vjs-volume-menu-button:before { + content: "\f107"; } + +.vjs-icon-fullscreen-enter, .video-js .vjs-fullscreen-control { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-fullscreen-enter:before, .video-js .vjs-fullscreen-control:before { + content: "\f108"; } + +.vjs-icon-fullscreen-exit, .video-js.vjs-fullscreen .vjs-fullscreen-control { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-fullscreen-exit:before, .video-js.vjs-fullscreen .vjs-fullscreen-control:before { + content: "\f109"; } + +.vjs-icon-square { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-square:before { + content: "\f10a"; } + +.vjs-icon-spinner { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-spinner:before { + content: "\f10b"; } + +.vjs-icon-subtitles, .video-js .vjs-subtitles-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-subtitles:before, .video-js .vjs-subtitles-button:before { + content: "\f10c"; } + +.vjs-icon-captions, .video-js .vjs-captions-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-captions:before, .video-js .vjs-captions-button:before { + content: "\f10d"; } + +.vjs-icon-chapters, .video-js .vjs-chapters-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-chapters:before, .video-js .vjs-chapters-button:before { + content: "\f10e"; } + +.vjs-icon-share { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-share:before { + content: "\f10f"; } + +.vjs-icon-cog { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-cog:before { + content: "\f110"; } + +.vjs-icon-circle, .video-js .vjs-mouse-display, .video-js .vjs-play-progress, .video-js .vjs-volume-level { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-circle:before, .video-js .vjs-mouse-display:before, .video-js .vjs-play-progress:before, .video-js .vjs-volume-level:before { + content: "\f111"; } + +.vjs-icon-circle-outline { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-circle-outline:before { + content: "\f112"; } + +.vjs-icon-circle-inner-circle { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-circle-inner-circle:before { + content: "\f113"; } + +.vjs-icon-hd { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-hd:before { + content: "\f114"; } + +.vjs-icon-cancel, .video-js .vjs-control.vjs-close-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-cancel:before, .video-js .vjs-control.vjs-close-button:before { + content: "\f115"; } + +.vjs-icon-replay { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-replay:before { + content: "\f116"; } + +.vjs-icon-facebook { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-facebook:before { + content: "\f117"; } + +.vjs-icon-gplus { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-gplus:before { + content: "\f118"; } + +.vjs-icon-linkedin { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-linkedin:before { + content: "\f119"; } + +.vjs-icon-twitter { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-twitter:before { + content: "\f11a"; } + +.vjs-icon-tumblr { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-tumblr:before { + content: "\f11b"; } + +.vjs-icon-pinterest { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-pinterest:before { + content: "\f11c"; } + +.vjs-icon-audio-description, .video-js .vjs-descriptions-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-audio-description:before, .video-js .vjs-descriptions-button:before { + content: "\f11d"; } + +.vjs-icon-audio, .video-js .vjs-audio-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-audio:before, .video-js .vjs-audio-button:before { + content: "\f11e"; } + +.video-js { + display: block; + vertical-align: top; + box-sizing: border-box; + color: #fff; + background-color: #000; + position: relative; + padding: 0; + font-size: 10px; + line-height: 1; + font-weight: normal; + font-style: normal; + font-family: Arial, Helvetica, sans-serif; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .video-js:-moz-full-screen { + position: absolute; } + .video-js:-webkit-full-screen { + width: 100% !important; + height: 100% !important; } + +.video-js *, +.video-js *:before, +.video-js *:after { + box-sizing: inherit; } + +.video-js ul { + font-family: inherit; + font-size: inherit; + line-height: inherit; + list-style-position: outside; + margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0; } + +.video-js.vjs-fluid, +.video-js.vjs-16-9, +.video-js.vjs-4-3 { + width: 100%; + max-width: 100%; + height: 0; } + +.video-js.vjs-16-9 { + padding-top: 56.25%; } + +.video-js.vjs-4-3 { + padding-top: 75%; } + +.video-js.vjs-fill { + width: 100%; + height: 100%; } + +.video-js .vjs-tech { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + +body.vjs-full-window { + padding: 0; + margin: 0; + height: 100%; + overflow-y: auto; } + +.vjs-full-window .video-js.vjs-fullscreen { + position: fixed; + overflow: hidden; + z-index: 1000; + left: 0; + top: 0; + bottom: 0; + right: 0; } + +.video-js.vjs-fullscreen { + width: 100% !important; + height: 100% !important; + padding-top: 0 !important; } + +.video-js.vjs-fullscreen.vjs-user-inactive { + cursor: none; } + +.vjs-hidden { + display: none !important; } + +.vjs-disabled { + opacity: 0.5; + cursor: default; } + +.video-js .vjs-offscreen { + height: 1px; + left: -9999px; + position: absolute; + top: 0; + width: 1px; } + +.vjs-lock-showing { + display: block !important; + opacity: 1; + visibility: visible; } + +.vjs-no-js { + padding: 20px; + color: #fff; + background-color: #000; + font-size: 18px; + font-family: Arial, Helvetica, sans-serif; + text-align: center; + width: 300px; + height: 150px; + margin: 0px auto; } + +.vjs-no-js a, +.vjs-no-js a:visited { + color: #66A8CC; } + +.video-js .vjs-big-play-button { + font-size: 3em; + line-height: 1.5em; + height: 1.5em; + width: 3em; + display: block; + position: absolute; + top: 10px; + left: 10px; + padding: 0; + cursor: pointer; + opacity: 1; + border: 0.06666em solid #fff; + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.7); + -webkit-border-radius: 0.3em; + -moz-border-radius: 0.3em; + border-radius: 0.3em; + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + -o-transition: all 0.4s; + transition: all 0.4s; } + +.vjs-big-play-centered .vjs-big-play-button { + top: 50%; + left: 50%; + margin-top: -0.75em; + margin-left: -1.5em; } + +.vjs-big-play-button, +.video-js .vjs-big-play-button:focus { + outline: 0; + border-color: #fff; + background-color: #73859f; + background-color: rgba(115, 133, 159, 0.5); + -webkit-transition: all 0s; + -moz-transition: all 0s; + -o-transition: all 0s; + transition: all 0s; } + +.vjs-controls-disabled .vjs-big-play-button, +.vjs-has-started .vjs-big-play-button, +.vjs-using-native-controls .vjs-big-play-button, +.vjs-error .vjs-big-play-button { + display: none; } + +.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause .vjs-big-play-button { + display: block; } + +.video-js button { + background: none; + border: none; + color: inherit; + display: inline-block; + overflow: visible; + font-size: inherit; + line-height: inherit; + text-transform: none; + text-decoration: none; + transition: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } + +.video-js .vjs-control.vjs-close-button { + cursor: pointer; + height: 3em; + position: absolute; + right: 0; + top: 0.5em; + z-index: 2; } + +.vjs-menu-button { + cursor: pointer; } + +.vjs-menu-button.vjs-disabled { + cursor: default; } + +.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu { + display: none; } + +.vjs-menu .vjs-menu-content { + display: block; + padding: 0; + margin: 0; + overflow: auto; + font-family: Arial, Helvetica, sans-serif; } + +.vjs-scrubbing .vjs-menu-button:hover .vjs-menu { + display: none; } + +.vjs-menu li { + list-style: none; + margin: 0; + padding: 0.2em 0; + line-height: 1.4em; + font-size: 1.2em; + text-align: center; + text-transform: lowercase; } + +.vjs-menu li.vjs-menu-item:focus, +.vjs-menu li.vjs-menu-item:hover { + outline: 0; + background-color: #73859f; + background-color: rgba(115, 133, 159, 0.5); } + +.vjs-menu li.vjs-selected, +.vjs-menu li.vjs-selected:focus, +.vjs-menu li.vjs-selected:hover { + background-color: #fff; + color: #2B333F; } + +.vjs-menu li.vjs-menu-title { + text-align: center; + text-transform: uppercase; + font-size: 1em; + line-height: 2em; + padding: 0; + margin: 0 0 0.3em 0; + font-weight: bold; + cursor: default; } + +.vjs-menu-button-popup .vjs-menu { + display: none; + position: absolute; + bottom: 0; + width: 10em; + left: -3em; + height: 0em; + margin-bottom: 1.5em; + border-top-color: rgba(43, 51, 63, 0.7); } + +.vjs-menu-button-popup .vjs-menu .vjs-menu-content { + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.7); + position: absolute; + width: 100%; + bottom: 1.5em; + max-height: 15em; } + +.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu, +.vjs-menu-button-popup .vjs-menu.vjs-lock-showing { + display: block; } + +.video-js .vjs-menu-button-inline { + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + -o-transition: all 0.4s; + transition: all 0.4s; + overflow: hidden; } + +.video-js .vjs-menu-button-inline:before { + width: 2.222222222em; } + +.video-js .vjs-menu-button-inline:hover, +.video-js .vjs-menu-button-inline:focus, +.video-js .vjs-menu-button-inline.vjs-slider-active, +.video-js.vjs-no-flex .vjs-menu-button-inline { + width: 12em; } + +.video-js .vjs-menu-button-inline.vjs-slider-active { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; } + +.vjs-menu-button-inline .vjs-menu { + opacity: 0; + height: 100%; + width: auto; + position: absolute; + left: 4em; + top: 0; + padding: 0; + margin: 0; + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + -o-transition: all 0.4s; + transition: all 0.4s; } + +.vjs-menu-button-inline:hover .vjs-menu, +.vjs-menu-button-inline:focus .vjs-menu, +.vjs-menu-button-inline.vjs-slider-active .vjs-menu { + display: block; + opacity: 1; } + +.vjs-no-flex .vjs-menu-button-inline .vjs-menu { + display: block; + opacity: 1; + position: relative; + width: auto; } + +.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu, +.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu, +.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu { + width: auto; } + +.vjs-menu-button-inline .vjs-menu-content { + width: auto; + height: 100%; + margin: 0; + overflow: hidden; } + +.video-js .vjs-control-bar { + display: none; + width: 100%; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3.0em; + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.7); } + +.vjs-has-started .vjs-control-bar { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + visibility: visible; + opacity: 1; + -webkit-transition: visibility 0.1s, opacity 0.1s; + -moz-transition: visibility 0.1s, opacity 0.1s; + -o-transition: visibility 0.1s, opacity 0.1s; + transition: visibility 0.1s, opacity 0.1s; } + +.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { + visibility: visible; + opacity: 0; + -webkit-transition: visibility 1s, opacity 1s; + -moz-transition: visibility 1s, opacity 1s; + -o-transition: visibility 1s, opacity 1s; + transition: visibility 1s, opacity 1s; } + +.vjs-controls-disabled .vjs-control-bar, +.vjs-using-native-controls .vjs-control-bar, +.vjs-error .vjs-control-bar { + display: none !important; } + +.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { + opacity: 1; + visibility: visible; } + +.vjs-has-started.vjs-no-flex .vjs-control-bar { + display: table; } + +.video-js .vjs-control { + outline: none; + position: relative; + text-align: center; + margin: 0; + padding: 0; + height: 100%; + width: 4em; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + .video-js .vjs-control:before { + font-size: 1.8em; + line-height: 1.67; } + +.video-js .vjs-control:focus:before, +.video-js .vjs-control:hover:before, +.video-js .vjs-control:focus { + text-shadow: 0em 0em 1em white; } + +.video-js .vjs-control-text { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + +.vjs-no-flex .vjs-control { + display: table-cell; + vertical-align: middle; } + +.video-js .vjs-custom-control-spacer { + display: none; } + +.video-js .vjs-progress-control { + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + min-width: 4em; } + +.vjs-live .vjs-progress-control { + display: none; } + +.video-js .vjs-progress-holder { + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; + height: 0.3em; } + +.video-js .vjs-progress-control:hover .vjs-progress-holder { + font-size: 1.666666666666666666em; } + +/* If we let the font size grow as much as everything else, the current time tooltip ends up + ginormous. If you'd like to enable the current time tooltip all the time, this should be disabled + to avoid a weird hitch when you roll off the hover. */ +.video-js .vjs-progress-control:hover .vjs-time-tooltip, +.video-js .vjs-progress-control:hover .vjs-mouse-display:after, +.video-js .vjs-progress-control:hover .vjs-play-progress:after { + font-family: Arial, Helvetica, sans-serif; + visibility: visible; + font-size: 0.6em; } + +.video-js .vjs-progress-holder .vjs-play-progress, +.video-js .vjs-progress-holder .vjs-load-progress, +.video-js .vjs-progress-holder .vjs-tooltip-progress-bar, +.video-js .vjs-progress-holder .vjs-load-progress div { + position: absolute; + display: block; + height: 100%; + margin: 0; + padding: 0; + width: 0; + left: 0; + top: 0; } + +.video-js .vjs-mouse-display:before { + display: none; } + +.video-js .vjs-play-progress { + background-color: #fff; } + .video-js .vjs-play-progress:before { + position: absolute; + top: -0.333333333333333em; + right: -0.5em; + font-size: 0.9em; } + +.video-js .vjs-time-tooltip, +.video-js .vjs-mouse-display:after, +.video-js .vjs-play-progress:after { + visibility: hidden; + pointer-events: none; + position: absolute; + top: -3.4em; + right: -1.9em; + font-size: 0.9em; + color: #000; + content: attr(data-current-time); + padding: 6px 8px 8px 8px; + background-color: #fff; + background-color: rgba(255, 255, 255, 0.8); + -webkit-border-radius: 0.3em; + -moz-border-radius: 0.3em; + border-radius: 0.3em; } + +.video-js .vjs-time-tooltip, +.video-js .vjs-play-progress:before, +.video-js .vjs-play-progress:after { + z-index: 1; } + +.video-js .vjs-progress-control .vjs-keep-tooltips-inside:after { + display: none; } + +.video-js .vjs-load-progress { + background: #bfc7d3; + background: rgba(115, 133, 159, 0.5); } + +.video-js .vjs-load-progress div { + background: white; + background: rgba(115, 133, 159, 0.75); } + +.video-js.vjs-no-flex .vjs-progress-control { + width: auto; } + +.video-js .vjs-time-tooltip { + display: inline-block; + height: 2.4em; + position: relative; + float: right; + right: -1.9em; } + +.vjs-tooltip-progress-bar { + visibility: hidden; } + +.video-js .vjs-progress-control .vjs-mouse-display { + display: none; + position: absolute; + width: 1px; + height: 100%; + background-color: #000; + z-index: 1; } + +.vjs-no-flex .vjs-progress-control .vjs-mouse-display { + z-index: 0; } + +.video-js .vjs-progress-control:hover .vjs-mouse-display { + display: block; } + +.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display, +.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display:after { + visibility: hidden; + opacity: 0; + -webkit-transition: visibility 1s, opacity 1s; + -moz-transition: visibility 1s, opacity 1s; + -o-transition: visibility 1s, opacity 1s; + transition: visibility 1s, opacity 1s; } + +.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display, +.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display:after { + display: none; } + +.vjs-mouse-display .vjs-time-tooltip, +.video-js .vjs-progress-control .vjs-mouse-display:after { + color: #fff; + background-color: #000; + background-color: rgba(0, 0, 0, 0.8); } + +.video-js .vjs-slider { + outline: 0; + position: relative; + cursor: pointer; + padding: 0; + margin: 0 0.45em 0 0.45em; + background-color: #73859f; + background-color: rgba(115, 133, 159, 0.5); } + +.video-js .vjs-slider:focus { + text-shadow: 0em 0em 1em white; + -webkit-box-shadow: 0 0 1em #fff; + -moz-box-shadow: 0 0 1em #fff; + box-shadow: 0 0 1em #fff; } + +.video-js .vjs-mute-control, +.video-js .vjs-volume-menu-button { + cursor: pointer; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + +.video-js .vjs-volume-control { + width: 5em; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; } + +.video-js .vjs-volume-bar { + margin: 1.35em 0.45em; } + +.vjs-volume-bar.vjs-slider-horizontal { + width: 5em; + height: 0.3em; } + +.vjs-volume-bar.vjs-slider-vertical { + width: 0.3em; + height: 5em; + margin: 1.35em auto; } + +.video-js .vjs-volume-level { + position: absolute; + bottom: 0; + left: 0; + background-color: #fff; } + .video-js .vjs-volume-level:before { + position: absolute; + font-size: 0.9em; } + +.vjs-slider-vertical .vjs-volume-level { + width: 0.3em; } + .vjs-slider-vertical .vjs-volume-level:before { + top: -0.5em; + left: -0.3em; } + +.vjs-slider-horizontal .vjs-volume-level { + height: 0.3em; } + .vjs-slider-horizontal .vjs-volume-level:before { + top: -0.3em; + right: -0.5em; } + +.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level { + height: 100%; } + +.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level { + width: 100%; } + +.vjs-menu-button-popup.vjs-volume-menu-button .vjs-menu { + display: block; + width: 0; + height: 0; + border-top-color: transparent; } + +.vjs-menu-button-popup.vjs-volume-menu-button-vertical .vjs-menu { + left: 0.5em; + height: 8em; } + +.vjs-menu-button-popup.vjs-volume-menu-button-horizontal .vjs-menu { + left: -2em; } + +.vjs-menu-button-popup.vjs-volume-menu-button .vjs-menu-content { + height: 0; + width: 0; + overflow-x: hidden; + overflow-y: hidden; } + +.vjs-volume-menu-button-vertical:hover .vjs-menu-content, +.vjs-volume-menu-button-vertical:focus .vjs-menu-content, +.vjs-volume-menu-button-vertical.vjs-slider-active .vjs-menu-content, +.vjs-volume-menu-button-vertical .vjs-lock-showing .vjs-menu-content { + height: 8em; + width: 2.9em; } + +.vjs-volume-menu-button-horizontal:hover .vjs-menu-content, +.vjs-volume-menu-button-horizontal:focus .vjs-menu-content, +.vjs-volume-menu-button-horizontal .vjs-slider-active .vjs-menu-content, +.vjs-volume-menu-button-horizontal .vjs-lock-showing .vjs-menu-content { + height: 2.9em; + width: 8em; } + +.vjs-volume-menu-button.vjs-menu-button-inline .vjs-menu-content { + background-color: transparent !important; } + +.vjs-poster { + display: inline-block; + vertical-align: middle; + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: contain; + background-color: #000000; + cursor: pointer; + margin: 0; + padding: 0; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + height: 100%; } + +.vjs-poster img { + display: block; + vertical-align: middle; + margin: 0 auto; + max-height: 100%; + padding: 0; + width: 100%; } + +.vjs-has-started .vjs-poster { + display: none; } + +.vjs-audio.vjs-has-started .vjs-poster { + display: block; } + +.vjs-using-native-controls .vjs-poster { + display: none; } + +.video-js .vjs-live-control { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: flex-start; + -webkit-align-items: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; + font-size: 1em; + line-height: 3em; } + +.vjs-no-flex .vjs-live-control { + display: table-cell; + width: auto; + text-align: left; } + +.video-js .vjs-time-control { + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1em; + line-height: 3em; + min-width: 2em; + width: auto; + padding-left: 1em; + padding-right: 1em; } + +.vjs-live .vjs-time-control { + display: none; } + +.video-js .vjs-current-time, +.vjs-no-flex .vjs-current-time { + display: none; } + +.video-js .vjs-duration, +.vjs-no-flex .vjs-duration { + display: none; } + +.vjs-time-divider { + display: none; + line-height: 3em; } + +.vjs-live .vjs-time-divider { + display: none; } + +.video-js .vjs-play-control { + cursor: pointer; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + +.vjs-text-track-display { + position: absolute; + bottom: 3em; + left: 0; + right: 0; + top: 0; + pointer-events: none; } + +.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display { + bottom: 1em; } + +.video-js .vjs-text-track { + font-size: 1.4em; + text-align: center; + margin-bottom: 0.1em; + background-color: #000; + background-color: rgba(0, 0, 0, 0.5); } + +.vjs-subtitles { + color: #fff; } + +.vjs-captions { + color: #fc6; } + +.vjs-tt-cue { + display: block; } + +video::-webkit-media-text-track-display { + -moz-transform: translateY(-3em); + -ms-transform: translateY(-3em); + -o-transform: translateY(-3em); + -webkit-transform: translateY(-3em); + transform: translateY(-3em); } + +.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display { + -moz-transform: translateY(-1.5em); + -ms-transform: translateY(-1.5em); + -o-transform: translateY(-1.5em); + -webkit-transform: translateY(-1.5em); + transform: translateY(-1.5em); } + +.video-js .vjs-fullscreen-control { + cursor: pointer; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + +.vjs-playback-rate .vjs-playback-rate-value { + font-size: 1.5em; + line-height: 2; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; } + +.vjs-playback-rate .vjs-menu { + width: 4em; + left: 0em; } + +.vjs-error .vjs-error-display .vjs-modal-dialog-content { + font-size: 1.4em; + text-align: center; } + +.vjs-error .vjs-error-display:before { + color: #fff; + content: 'X'; + font-family: Arial, Helvetica, sans-serif; + font-size: 4em; + left: 0; + line-height: 1; + margin-top: -0.5em; + position: absolute; + text-shadow: 0.05em 0.05em 0.1em #000; + text-align: center; + top: 50%; + vertical-align: middle; + width: 100%; } + +.vjs-loading-spinner { + display: none; + position: absolute; + top: 50%; + left: 50%; + margin: -25px 0 0 -25px; + opacity: 0.85; + text-align: left; + border: 6px solid rgba(43, 51, 63, 0.7); + box-sizing: border-box; + background-clip: padding-box; + width: 50px; + height: 50px; + border-radius: 25px; } + +.vjs-seeking .vjs-loading-spinner, +.vjs-waiting .vjs-loading-spinner { + display: block; } + +.vjs-loading-spinner:before, +.vjs-loading-spinner:after { + content: ""; + position: absolute; + margin: -6px; + box-sizing: inherit; + width: inherit; + height: inherit; + border-radius: inherit; + opacity: 1; + border: inherit; + border-color: transparent; + border-top-color: white; } + +.vjs-seeking .vjs-loading-spinner:before, +.vjs-seeking .vjs-loading-spinner:after, +.vjs-waiting .vjs-loading-spinner:before, +.vjs-waiting .vjs-loading-spinner:after { + -webkit-animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite; + animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite; } + +.vjs-seeking .vjs-loading-spinner:before, +.vjs-waiting .vjs-loading-spinner:before { + border-top-color: white; } + +.vjs-seeking .vjs-loading-spinner:after, +.vjs-waiting .vjs-loading-spinner:after { + border-top-color: white; + -webkit-animation-delay: 0.44s; + animation-delay: 0.44s; } + +@keyframes vjs-spinner-spin { + 100% { + transform: rotate(360deg); } } + +@-webkit-keyframes vjs-spinner-spin { + 100% { + -webkit-transform: rotate(360deg); } } + +@keyframes vjs-spinner-fade { + 0% { + border-top-color: #73859f; } + 20% { + border-top-color: #73859f; } + 35% { + border-top-color: white; } + 60% { + border-top-color: #73859f; } + 100% { + border-top-color: #73859f; } } + +@-webkit-keyframes vjs-spinner-fade { + 0% { + border-top-color: #73859f; } + 20% { + border-top-color: #73859f; } + 35% { + border-top-color: white; } + 60% { + border-top-color: #73859f; } + 100% { + border-top-color: #73859f; } } + +.vjs-chapters-button .vjs-menu ul { + width: 24em; } + +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-custom-control-spacer { + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; } + +.video-js.vjs-layout-tiny:not(.vjs-fullscreen).vjs-no-flex .vjs-custom-control-spacer { + width: auto; } + +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-remaining-time, +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-playback-rate, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-progress-control, +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-volume-control, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-volume-menu-button, +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-descriptions-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-captions-button, +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-subtitles-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-audio-button { + display: none; } + +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-remaining-time, +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-playback-rate, +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-volume-control, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-volume-menu-button, +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-descriptions-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-captions-button, +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-subtitles-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-audio-button { + display: none; } + +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-remaining-time, +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-playback-rate, +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-volume-control, +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-descriptions-button, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-captions-button, +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-subtitles-button .vjs-audio-button { + display: none; } + +.vjs-caption-settings { + position: relative; + top: 1em; + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.75); + color: #fff; + margin: 0 auto; + padding: 0.5em; + height: 16em; + font-size: 12px; + width: 40em; } + +.vjs-caption-settings .vjs-tracksettings { + top: 0; + bottom: 1em; + left: 0; + right: 0; + position: absolute; + overflow: auto; } + +.vjs-caption-settings .vjs-tracksettings-colors, +.vjs-caption-settings .vjs-tracksettings-font { + float: left; } + +.vjs-caption-settings .vjs-tracksettings-colors:after, +.vjs-caption-settings .vjs-tracksettings-font:after, +.vjs-caption-settings .vjs-tracksettings-controls:after { + clear: both; } + +.vjs-caption-settings .vjs-tracksettings-controls { + position: absolute; + bottom: 1em; + right: 1em; } + +.vjs-caption-settings .vjs-tracksetting { + margin: 5px; + padding: 3px; + min-height: 40px; + border: none; } + +.vjs-caption-settings .vjs-tracksetting label, +.vjs-caption-settings .vjs-tracksetting legend { + display: block; + width: 100px; + margin-bottom: 5px; } + +.vjs-caption-settings .vjs-tracksetting span { + display: inline; + margin-left: 5px; + vertical-align: top; + float: right; } + +.vjs-caption-settings .vjs-tracksetting > div { + margin-bottom: 5px; + min-height: 20px; } + +.vjs-caption-settings .vjs-tracksetting > div:last-child { + margin-bottom: 0; + padding-bottom: 0; + min-height: 0; } + +.vjs-caption-settings label > input { + margin-right: 10px; } + +.vjs-caption-settings fieldset { + margin-top: 1em; + margin-left: .5em; } + +.vjs-caption-settings fieldset .vjs-label { + position: absolute; + clip: rect(1px 1px 1px 1px); + /* for Internet Explorer */ + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; } + +.vjs-caption-settings input[type="button"] { + width: 40px; + height: 40px; } + +.video-js .vjs-modal-dialog { + background: rgba(0, 0, 0, 0.8); + background: -webkit-linear-gradient(-90deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0)); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0)); } + +.vjs-modal-dialog .vjs-modal-dialog-content { + font-size: 1.2em; + line-height: 1.5; + padding: 20px 24px; + z-index: 1; } + +@media print { + .video-js > *:not(.vjs-tech):not(.vjs-poster) { + visibility: hidden; } } + +@media \0screen { + .vjs-user-inactive.vjs-playing .vjs-control-bar :before { + content: ""; + } +} + +@media \0screen { + .vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { + visibility: hidden; + } +} diff --git a/server/public/css/videojs-sublime-skin.css b/server/public/css/videojs-sublime-skin.css new file mode 100644 index 00000000..7be7cee6 --- /dev/null +++ b/server/public/css/videojs-sublime-skin.css @@ -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 +} diff --git a/server/public/img/bucket.svg b/server/public/img/bucket.svg new file mode 100644 index 00000000..8380a9bb --- /dev/null +++ b/server/public/img/bucket.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/server/public/img/delete.svg b/server/public/img/delete.svg new file mode 100644 index 00000000..f7358b02 --- /dev/null +++ b/server/public/img/delete.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/public/img/download.svg b/server/public/img/download.svg new file mode 100644 index 00000000..ee148bd4 --- /dev/null +++ b/server/public/img/download.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/server/public/img/dropbox.png b/server/public/img/dropbox.png new file mode 100644 index 0000000000000000000000000000000000000000..19515de3e14cf257cc4089f8f2d0bab5cb104b5a GIT binary patch literal 9130 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 + + + + + + + + diff --git a/server/public/img/error.svg b/server/public/img/error.svg new file mode 100644 index 00000000..e5bcec14 --- /dev/null +++ b/server/public/img/error.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/public/img/file.svg b/server/public/img/file.svg new file mode 100644 index 00000000..3803ac02 --- /dev/null +++ b/server/public/img/file.svg @@ -0,0 +1,4 @@ + + + + diff --git a/server/public/img/folder copy.svg b/server/public/img/folder copy.svg new file mode 100644 index 00000000..f1b08731 --- /dev/null +++ b/server/public/img/folder copy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/server/public/img/folder.svg b/server/public/img/folder.svg new file mode 100644 index 00000000..f1b08731 --- /dev/null +++ b/server/public/img/folder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/server/public/img/google-drive.png b/server/public/img/google-drive.png new file mode 100644 index 0000000000000000000000000000000000000000..7d0135b05eb672cfe0f9035a79733f492fdd5c06 GIT binary patch literal 10710 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 p{D1ki;eWqU + + + + + diff --git a/server/public/img/loader.svg b/server/public/img/loader.svg new file mode 100644 index 00000000..f997200a --- /dev/null +++ b/server/public/img/loader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/public/img/loader_white.svg b/server/public/img/loader_white.svg new file mode 100644 index 00000000..ad3b741a --- /dev/null +++ b/server/public/img/loader_white.svg @@ -0,0 +1 @@ + diff --git a/server/public/img/logo.png b/server/public/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ac840fc15cf55d822215fd8a5b2dc678dc0426dc GIT binary patch literal 9525 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/public/img/play.svg b/server/public/img/play.svg new file mode 100644 index 00000000..f22527cf --- /dev/null +++ b/server/public/img/play.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/public/img/power.svg b/server/public/img/power.svg new file mode 100644 index 00000000..06e7e0ba --- /dev/null +++ b/server/public/img/power.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/public/img/save.svg b/server/public/img/save.svg new file mode 100644 index 00000000..6cec20d4 --- /dev/null +++ b/server/public/img/save.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/public/index.html b/server/public/index.html new file mode 100644 index 00000000..ac9a5739 --- /dev/null +++ b/server/public/index.html @@ -0,0 +1 @@ + Nuage
\ No newline at end of file diff --git a/server/utils/crypto.js b/server/utils/crypto.js new file mode 100644 index 00000000..d6f6f1f3 --- /dev/null +++ b/server/utils/crypto.js @@ -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; + } +} diff --git a/server/utils/mimetype.js b/server/utils/mimetype.js new file mode 100644 index 00000000..4e4eca89 --- /dev/null +++ b/server/utils/mimetype.js @@ -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" +} diff --git a/src/client.js b/src/client.js new file mode 100644 index 00000000..8c9941aa --- /dev/null +++ b/src/client.js @@ -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(, document.getElementById('main')); +}; diff --git a/src/components/api.js b/src/components/api.js new file mode 100644 index 00000000..aaf40464 --- /dev/null +++ b/src/components/api.js @@ -0,0 +1 @@ +import React from 'react'; diff --git a/src/components/breadcrumb.js b/src/components/breadcrumb.js new file mode 100644 index 00000000..d06cd042 --- /dev/null +++ b/src/components/breadcrumb.js @@ -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 ( +
+ + + { + this.state.path.map((path, index) => { + return ( + + + + + ) + }) + } + +
+ ); + } +} + +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 ( +
+
    + {props.children} +
+
+ ); +} +const Logout = (props) => { + let style = { + float: 'right', + fontSize: '17px', + display: 'inline-block', + padding: '6px 0px 6px 6px', + margin: '0 0px' + } + return ( +
  • + + + +
  • + ); +} + +const Saving = (props) => { + let style = { + display: 'inline', + padding: '0 3px' + }; + + if(props.needSaving){ + return ( + + * + + ); + }else{ + return null; + } +} + +const Separator = (props) => { + return ( + + > + + ); +} + + +@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 ( +
  • + {this.props.path.label} + +
  • + ); + } +} + + +// 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 ( +
    + +
    + ) + } +} diff --git a/src/components/connect.js b/src/components/connect.js new file mode 100644 index 00000000..c0bb4ed5 --- /dev/null +++ b/src/components/connect.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export class Connect extends React.Component { + constructor(props){ + super(props); + } + + render() { + return ( +
    + CONNECT +
    + ); + } +} diff --git a/src/components/editor.js b/src/components/editor.js new file mode 100644 index 00000000..63cf8691 --- /dev/null +++ b/src/components/editor.js @@ -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 ( +
    + ); + } +} + +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; +// } diff --git a/src/components/filesystem.js b/src/components/filesystem.js new file mode 100644 index 00000000..165b3de1 --- /dev/null +++ b/src/components/filesystem.js @@ -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( +
    + + {this.setState({sort: value})}} accessRight={this.state.access_right}> + + + + 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 + } + }) + } + + + There is nothing here + + +
    + ); + } +} + +FileSystem.PropTypes = { + path: PropTypes.string.isRequired, + files: PropTypes.array.isRequired +} diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 00000000..de6d7238 --- /dev/null +++ b/src/components/index.js @@ -0,0 +1,4 @@ +export { BreadCrumb } from './breadcrumb'; +export { Editor } from './editor'; +export { FileSystem } from './filesystem'; +export { Connect } from './connect'; diff --git a/src/data/api.js b/src/data/api.js new file mode 100644 index 00000000..c6bef894 --- /dev/null +++ b/src/data/api.js @@ -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(); diff --git a/src/data/events.js b/src/data/events.js new file mode 100644 index 00000000..9bdd2243 --- /dev/null +++ b/src/data/events.js @@ -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 ; + } + } +} + + + +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 ; + } + } +} diff --git a/src/data/index.js b/src/data/index.js new file mode 100644 index 00000000..2d14b4ac --- /dev/null +++ b/src/data/index.js @@ -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'; diff --git a/src/data/mimetype.js b/src/data/mimetype.js new file mode 100644 index 00000000..bd47d79a --- /dev/null +++ b/src/data/mimetype.js @@ -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" +} diff --git a/src/data/password.js b/src/data/password.js new file mode 100644 index 00000000..54bf5fff --- /dev/null +++ b/src/data/password.js @@ -0,0 +1,14 @@ +function keyManager(){ + let key = null; + return { + get: function(){ + return key; + }, + set: function(_key){ + key = _key || null + } + } +} + + +export const password = keyManager(); diff --git a/src/data/tools.js b/src/data/tools.js new file mode 100644 index 00000000..4ef2cccb --- /dev/null +++ b/src/data/tools.js @@ -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); + }); +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 00000000..663d2553 --- /dev/null +++ b/src/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + Nuage + + + + +
    + + + + + diff --git a/src/pages/connectpage.js b/src/pages/connectpage.js new file mode 100644 index 00000000..fab44610 --- /dev/null +++ b/src/pages/connectpage.js @@ -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 ( +
    + + + + + + + +
    + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} + +Textarea.propTypes = { + type: PropTypes.string, + placeholder: PropTypes.string +}; diff --git a/src/utilities/theme.js b/src/utilities/theme.js new file mode 100644 index 00000000..332e38d7 --- /dev/null +++ b/src/utilities/theme.js @@ -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' + } +} diff --git a/src/utilities/uploader.js b/src/utilities/uploader.js new file mode 100644 index 00000000..815b9f0c --- /dev/null +++ b/src/utilities/uploader.js @@ -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( +
    + + DRAG FILE HERE + +
    + {this.props.children} +
    +
    + ); + } +} +Uploader.PropTypes = { + path: PropTypes.string.isRequired, + onUpload: PropTypes.func.isRequired +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..f8b653b1 --- /dev/null +++ b/webpack.config.js @@ -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;