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 00000000..19515de3 Binary files /dev/null and b/server/public/img/dropbox.png differ diff --git a/server/public/img/edit.svg b/server/public/img/edit.svg new file mode 100644 index 00000000..463b377e --- /dev/null +++ b/server/public/img/edit.svg @@ -0,0 +1,9 @@ + + + + + + + + + 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 00000000..7d0135b0 Binary files /dev/null and b/server/public/img/google-drive.png differ diff --git a/server/public/img/link.svg b/server/public/img/link.svg new file mode 100644 index 00000000..f1b08731 --- /dev/null +++ b/server/public/img/link.svg @@ -0,0 +1,6 @@ + + + + + + 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 00000000..ac840fc1 Binary files /dev/null and b/server/public/img/logo.png differ diff --git a/server/public/img/logo_large.png b/server/public/img/logo_large.png new file mode 100644 index 00000000..628f6254 Binary files /dev/null and b/server/public/img/logo_large.png differ diff --git a/server/public/img/pause.svg b/server/public/img/pause.svg new file mode 100644 index 00000000..7c8b0090 --- /dev/null +++ b/server/public/img/pause.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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;