├── .nvmrc ├── plop ├── nodemon ├── test.js ├── prototype.js ├── testfs.html └── server.js ├── bin ├── filesync-relay └── filesync-server ├── .bowerrc ├── plop.js ├── scripts └── changelog ├── public ├── app │ ├── app.js │ ├── HistoryCtrl.js │ ├── HistoryService.js │ ├── VisibilityService.js │ ├── SocialCtrl.js │ └── SocketIOService.js ├── event.js ├── css │ └── style.css └── index.html ├── config.js ├── .gitignore ├── .checkbuild ├── bower.json ├── package.json ├── CHANGELOG.md ├── relay.js ├── README.md ├── server.js └── .jshintrc /.nvmrc: -------------------------------------------------------------------------------- 1 | v0.10.35 -------------------------------------------------------------------------------- /plop/nodemon: -------------------------------------------------------------------------------- 1 | ../node_modules/.bin/nodemon -------------------------------------------------------------------------------- /bin/filesync-relay: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | node ../relay.js $1 3 | -------------------------------------------------------------------------------- /bin/filesync-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | node ../server.js $1 3 | -------------------------------------------------------------------------------- /plop/test.js: -------------------------------------------------------------------------------- 1 | setInterval(function() { 2 | console.log(new Date().toString()); 3 | }, 100); 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/components/", 3 | "analytics": false, 4 | "timeout": 120000 5 | } -------------------------------------------------------------------------------- /plop.js: -------------------------------------------------------------------------------- 1 | console.log(process.argv); 2 | sqdoifjsdoifj 3 | sdfsdfqqs 4 | qsqsqs 5 | qsdqdsd 6 | qdsd 7 | qsqs 8 | -------------------------------------------------------------------------------- /scripts/changelog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # gem install github_changelog_generator 3 | github_changelog_generator -u fgribreau -p filesync 4 | -------------------------------------------------------------------------------- /public/app/app.js: -------------------------------------------------------------------------------- 1 | /*globals io, Visibility, _ */ 2 | 'use strict'; 3 | angular.module('FileSync', ['ngAnimate', 'hljs']); 4 | 5 | angular.module('FileSync') 6 | .constant('io', io) 7 | .constant('Visibility', Visibility) 8 | .constant('_', _); 9 | -------------------------------------------------------------------------------- /public/event.js: -------------------------------------------------------------------------------- 1 | // https://nodejs.org/api/events.html 2 | var EventEmitter = require('events').EventEmitter; 3 | var em = new EventEmitter(); 4 | 5 | em.on2('changed', function(value){ 6 | console.log('ok', value); 7 | }); 8 | 9 | em.emit('plop', 'sodk', 'dfdf', 'sddfdf'); 10 | 11 | em.emit('changed', true); 12 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function (logger) { 3 | var env = require('common-env/withLogger')(logger); 4 | 5 | return env.getOrElseAll({ 6 | auth: { 7 | token: 'please-define-one' 8 | }, 9 | server: { 10 | exposed_endpoint: 'http://127.0.0.1:3000', 11 | port: 3000 12 | }, 13 | relay: { 14 | 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /public/app/HistoryCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('FileSync').controller('HistoryCtrl', ['HistoryService', 'VisibilityService', 3 | function (HistoryService, VisibilityService) { 4 | this.edits = HistoryService.edits; 5 | this.visibility = VisibilityService; 6 | 7 | this.remove = function (edit) { 8 | HistoryService.remove(edit); 9 | }; 10 | } 11 | ]); 12 | -------------------------------------------------------------------------------- /public/app/HistoryService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('FileSync') 3 | .factory('HistoryService', function (SocketIOService, _) { 4 | var edits = []; 5 | 6 | SocketIOService.onFileChanged(function (filename, timestamp, content) { 7 | edits.unshift({ 8 | filename: filename, 9 | timestamp: timestamp, 10 | content: content 11 | }); 12 | }); 13 | 14 | return { 15 | edits: edits, 16 | remove: function (edit) { 17 | _.remove(edits, edit); 18 | } 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /plop/prototype.js: -------------------------------------------------------------------------------- 1 | // http://nodeguide.com/beginner.html 2 | // http://visionqmedia.github.io/masteringnode/book.html 3 | // http://stackoverflow.com/questions/2353818/how-do-i-get-started-with-node-js 4 | // http://www.html5rocks.com/en/tutorials/file/filesystem/ 5 | 6 | var a = { 7 | x: 10, 8 | calculate: function(z) { 9 | return this.x + this.y + z 10 | } 11 | }; 12 | 13 | var b = { 14 | y: 20, 15 | __proto__: a 16 | }; 17 | 18 | var c = { 19 | y: 30, 20 | __proto__: a 21 | }; 22 | 23 | // appel de la méthode héritée 24 | b.calculate(30); // 60 25 | c.calculate(40); // 80 26 | -------------------------------------------------------------------------------- /public/app/VisibilityService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('FileSync') 3 | .factory('VisibilityService', ['Visibility', 'SocketIOService', 4 | function (Visibility, SocketIOService) { 5 | Visibility.change(function (evt, state) { 6 | // state === 'hidden' || 'visible' 7 | SocketIOService.userChangedState(state); 8 | }); 9 | 10 | SocketIOService.userChangedState('visible'); 11 | 12 | var service = { 13 | states: {} 14 | }; 15 | 16 | SocketIOService.onVisibilityStatesChanged(function (states) { 17 | service.states = states; 18 | }); 19 | 20 | return service; 21 | } 22 | ]); 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | .env 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | 31 | public/components 32 | .npmignore 33 | -------------------------------------------------------------------------------- /public/app/SocialCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular 3 | .module('FileSync') 4 | .controller('SocialCtrl', ['$scope', 'SocketIOService', function($scope, SocketIOService) { 5 | this.viewers = []; 6 | this.plop = ''; 7 | this.messages = []; // chat messages 8 | this.message = ''; // current message to send 9 | 10 | SocketIOService.onChatMessage(function(message){ 11 | this.messages.push(message); 12 | $scope.$apply(); 13 | }.bind(this)); 14 | 15 | this.sendMessage = function() { 16 | SocketIOService.sendChatMessage(this.message); 17 | }; 18 | 19 | function onViewersUpdated(viewers) { 20 | this.viewers = viewers; 21 | $scope.$apply(); 22 | } 23 | 24 | SocketIOService.onViewersUpdated(onViewersUpdated.bind(this)); 25 | }]); 26 | -------------------------------------------------------------------------------- /.checkbuild: -------------------------------------------------------------------------------- 1 | { 2 | "checkbuild": { 3 | "enable": ["david", "jshint", "buddyjs", "nsp"], 4 | // don't exit immediately if one of the tools reports an error (default true) 5 | "continueOnError": true, 6 | // don't exit(1) even if we had some failures (default false) 7 | "allowFailures": false 8 | }, 9 | "jshint": { 10 | "args": ["public/app/**/*.js"] 11 | }, 12 | "plato": { 13 | "args": ["public/app/**/*.js"] 14 | }, 15 | "jscs": { 16 | "args": ["public/app/**/*.js"] 17 | }, 18 | "jsinspect": { 19 | "args": ["public/app/**/*.js"], 20 | "diff": true 21 | }, 22 | "buddyjs": { 23 | "args": ["public/app/**/*.js"], 24 | "ignore": [0, 1, 200] 25 | }, 26 | "david": { 27 | "ignore": ["bower"], 28 | "error": { 29 | "ESCM": false 30 | } 31 | }, 32 | "nsp": {} 33 | } 34 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filesync", 3 | "version": "2.0.0", 4 | "homepage": "https://github.com/FGRibreau/filesync", 5 | "authors": [ 6 | "FG Ribreau " 7 | ], 8 | "description": "Unidirectional 1-N file syncing with history and local merging", 9 | "main": "public/app/app.js", 10 | "license": "MIT", 11 | "private": true, 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "public/components/", 17 | "test", 18 | "tests" 19 | ], 20 | "dependencies": { 21 | "bootstrap": "~3.3.2", 22 | "angular-animate": "~1.3.14", 23 | "angular": "~1.3.14", 24 | "lodash": "~3.4.0", 25 | "jquery": "~2.1.3", 26 | "visibilityjs": "~1.2.1", 27 | "moment": "~2.9.0", 28 | "highlightjs": "~8.4.0", 29 | "angular-highlightjs": "~0.4.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /plop/testfs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
 5 | 
6 | 7 | 32 | -------------------------------------------------------------------------------- /plop/server.js: -------------------------------------------------------------------------------- 1 | function Gaze() { 2 | this.a = 1; 3 | } 4 | 5 | 6 | Gaze.prototype.watched = function() { 7 | console.log(this.a); 8 | }; 9 | 10 | var gaze = new Gaze(); 11 | gaze.watched(); 12 | 13 | var gaze = require('gaze'); 14 | 15 | gaze('/tmp/**.js', function(err, watcher) { 16 | if (err) { 17 | throw err; 18 | } 19 | 20 | // Get all watched files 21 | this.watched(function(err, watched) { 22 | console.log(watched); 23 | }); 24 | 25 | // On changed/added/deleted 26 | this.on('all', function(event, filepath) { 27 | console.log(filepath + ' was ' + event); 28 | }); 29 | 30 | // this.emit('all', 'destroyedForever', '/'); 31 | 32 | // var EE = require('events').EventEmitter; 33 | // eventEmitter.on('click', functiona(wasClicked){console.log('was clicked:', wasClicked);}); 34 | // eventEmitter.emit('click2', true); 35 | }); 36 | 37 | // var http = require('http'); 38 | // gaze('./**', function(err, watcher) { 39 | // if (err) { 40 | // throw err; 41 | // } 42 | // 43 | // // Get all watched files 44 | // this.watched(function(err, watched) { 45 | // console.log(watched); 46 | // }); 47 | // 48 | // }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filesync", 3 | "version": "2.2.0", 4 | "description": "[WorkInProgress - ProofOfConcept] Unidirectional 1-N file syncing with history and local merging", 5 | "scripts": { 6 | "start": "./bin/filesync", 7 | "postinstall": "bower install", 8 | "prepublish": "[ -r ~/.global.npmignore ] && cat ~/.global.npmignore > .npmignore;cat .gitignore >> .npmignore;" 9 | }, 10 | "bin": { 11 | "filesync-relay": "./bin/filesync-relay", 12 | "filesync-server": "./bin/filesync-server" 13 | }, 14 | "author": "Francois-Guillaume Ribreau (http://fgribreau.com/)", 15 | "bugs": { 16 | "url": "https://github.com/FGRibreau/filesync/issues" 17 | }, 18 | "homepage": "https://github.com/FGRibreau/filesync", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/FGRibreau/filesync.git" 22 | }, 23 | "license": "MIT", 24 | "dependencies": { 25 | "async": "1.*.*", 26 | "body-parser": "^1.12.0", 27 | "bower": "^1.3.12", 28 | "common-env": "4.*.*", 29 | "express": "^4.12.2", 30 | "gaze": "^0.5.1", 31 | "lodash": "^3.3.1", 32 | "socket.io": "^1.3.5", 33 | "socket.io-client": "^1.3.5", 34 | "winston": "1.*.*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v2.1.0](https://github.com/fgribreau/filesync/tree/v2.1.0) (2015-06-13) 4 | 5 | [Full Changelog](https://github.com/fgribreau/filesync/compare/v2.0.1...v2.1.0) 6 | 7 | **Closed issues:** 8 | 9 | - Impossible installation on Windows 8 [\#26](https://github.com/FGRibreau/filesync/issues/26) 10 | 11 | ## [v2.0.1](https://github.com/fgribreau/filesync/tree/v2.0.1) (2015-06-13) 12 | 13 | [Full Changelog](https://github.com/fgribreau/filesync/compare/v2.0.0...v2.0.1) 14 | 15 | **Merged pull requests:** 16 | 17 | - Fix bower install bug on Windows 8 [\#27](https://github.com/FGRibreau/filesync/pull/27) ([Xartok](https://github.com/Xartok)) 18 | 19 | - Update setup paragraph in Readme.md [\#25](https://github.com/FGRibreau/filesync/pull/25) ([Xartok](https://github.com/Xartok)) 20 | 21 | - Correction affichage zone texte. [\#13](https://github.com/FGRibreau/filesync/pull/13) ([Varadiell](https://github.com/Varadiell)) 22 | 23 | - code syntax highlight [\#12](https://github.com/FGRibreau/filesync/pull/12) ([rbaumier](https://github.com/rbaumier)) 24 | 25 | ## [v2.0.0](https://github.com/fgribreau/filesync/tree/v2.0.0) (2015-03-07) 26 | 27 | 28 | 29 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | .row-fluid{ 2 | clear: both; 3 | } 4 | 5 | .line{ 6 | margin-bottom:10px; 7 | padding: 4px; 8 | } 9 | 10 | .line textarea{ 11 | width: 100%; 12 | height:100px; 13 | } 14 | 15 | .line .cell{ 16 | padding:10px; 17 | color: #333; 18 | } 19 | 20 | .line .filename{ 21 | font-weight: 500; 22 | } 23 | 24 | .line .options{ 25 | text-align: center; 26 | } 27 | 28 | .animate.ng-enter, 29 | .animate.ng-leave { 30 | -webkit-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 31 | -moz-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 32 | -ms-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 33 | -o-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 34 | transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 35 | position: relative; 36 | display: block; 37 | } 38 | 39 | .animate.ng-leave.ng-leave-active, 40 | .animate.ng-enter { 41 | -webkit-transform: scaleY(0); 42 | -moz-transform: scaleY(0); 43 | -ms-transform: scaleY(0); 44 | -o-transform: scaleY(0); 45 | transform: scaleY(0); 46 | height: 0px; 47 | opacity: 0; 48 | } 49 | 50 | .animate.ng-enter.ng-enter-active, 51 | .animate.ng-leave { 52 | -webkit-transform: scaleY(1); 53 | -moz-transform: scaleY(1); 54 | -ms-transform: scaleY(1); 55 | -o-transform: scaleY(1); 56 | transform: scaleY(1); 57 | height: 30px; 58 | opacity: 1; 59 | } 60 | -------------------------------------------------------------------------------- /public/app/SocketIOService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('FileSync') 3 | .factory('SocketIOService', ['io', '_', '$timeout', function(io, _, $timeout) { 4 | var socket = io(); 5 | window.socket = socket; 6 | var _onFileChanged = _.noop; 7 | var _onVisibilityStatesChanged = _.noop; 8 | 9 | socket.on('connect', function() { 10 | console.log('connected'); 11 | var login = prompt('Nickname?'); 12 | socket.emit('viewer:new', login); 13 | }); 14 | 15 | 16 | 17 | socket.on('file:changed', function(filename, timestamp, content) { 18 | $timeout(function() { 19 | _onFileChanged(filename, timestamp, content); 20 | }); 21 | }); 22 | 23 | socket.on('users:visibility-states', function(states) { 24 | $timeout(function() { 25 | _onVisibilityStatesChanged(states); 26 | }); 27 | }); 28 | 29 | socket.on('error:auth', function(err) { 30 | // @todo yeurk 31 | alert(err); 32 | }); 33 | 34 | return { 35 | onChatMessage: function(f){ 36 | socket.on('message', f); 37 | }, 38 | 39 | sendChatMessage: function(message){ 40 | socket.emit('message', message); 41 | }, 42 | 43 | onViewersUpdated: function(f) { 44 | socket.on('viewers:updated', f); 45 | }, 46 | 47 | onFileChanged: function(f) { 48 | _onFileChanged = f; 49 | }, 50 | 51 | onVisibilityStatesChanged: function(f) { 52 | _onVisibilityStatesChanged = f; 53 | }, 54 | 55 | userChangedState: function(state) { 56 | socket.emit('user-visibility:changed', state); 57 | } 58 | }; 59 | }]); 60 | -------------------------------------------------------------------------------- /relay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var io = require('socket.io-client'); 3 | var gaze = require('gaze'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var logger = require('winston'); 7 | var config = require('./config')(logger); 8 | 9 | var directory = path.resolve(__dirname, process.argv[2]); 10 | 11 | if (!directory) { 12 | logger.error("Usage: node server.js /path/to/directory/**"); 13 | process.exit(1); 14 | } 15 | 16 | logger.info('listening on %s', directory); 17 | 18 | var SOCKET_IO_URL = config.server.exposed_endpoint + '/?access_token=' + config.auth.token; 19 | 20 | 21 | if(!directory.includes('**')){ 22 | logger.error("Directory should ends with /**, or /**/*(!(exclude_pattern.js))"); 23 | process.exit(1); 24 | } 25 | 26 | var DIRECTORY_BASE = path.dirname(directory).replace('**', ''); 27 | 28 | logger.info('connecting...'); 29 | var sio = io(SOCKET_IO_URL, { 30 | transports: ['polling'], 31 | multiplex: false 32 | }); 33 | 34 | sio.on('connect', function() { 35 | logger.info('connected!'); 36 | }); 37 | 38 | 39 | gaze(directory, function(err, watcher) { 40 | console.log('1'); 41 | if (err) { 42 | throw err; 43 | } 44 | 45 | // Get all watched files 46 | this.watched(function(err, watched) { 47 | console.log(watched); 48 | }); 49 | 50 | // On file changed 51 | this.on('changed', function(filepath) { 52 | sio.emit('file:changed', 53 | filepath.replace(DIRECTORY_BASE, ''), 54 | Date.now(), 55 | fs.readFileSync(filepath, 'utf-8') // @todo use async mode 56 | ); 57 | }); 58 | 59 | // On file added 60 | this.on('added', function(filepath) { 61 | console.log(filepath + ' was added'); 62 | }); 63 | 64 | // On file deleted 65 | this.on('deleted', function(filepath) { 66 | console.log(filepath + ' was deleted'); 67 | }); 68 | 69 | // On changed/added/deleted 70 | this.on('all', function(event, filepath) { 71 | console.log(filepath + ' was ' + event); 72 | }); 73 | 74 | // Get watched files with relative paths 75 | this.relative(function(err, files) { 76 | console.log(files); 77 | }); 78 | 79 | }); 80 | 81 | console.log('2'); 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Unidirectional 1-N file syncing with history and *local merging* [![Build Status](https://drone.io/github.com/FGRibreau/filesync/status.png)](https://drone.io/github.com/FGRibreau/filesync/latest) 2 | ================================================================ 3 | 4 | This is a **proof of concept** AND **a work in progress**, don't share the word, yet. 5 | 6 | # Motivation 7 | 8 | > “We are in 2015 and my students still have to copy what I wrote on a screen while I teach them something. This is a long and tedious process that slow down the lectures. 9 | > 10 | > Google Document-like tools should NEVER be used for sharing code, we want syntax highlighting and static analysis not copy/pasting code inside an online document each time we make a change. 11 | > 12 | > Online IDEs are NOT a solution. I have my own finely tuned editor, my students have theirs, we don't want to temporarily trade our workflow comfort for a lecture. 13 | > 14 | > Both solution are stupid. 15 | > 16 | > I want a tool that will allow each student to retrieve in real-time my edits while keeping their own local modifications. This tool will work with any editor/IDE because we sync at the file-system level. Each modification will be displayed in an history log and would be either merged locally or dropped definitely.” 17 | 18 | > — 03/06/2015 19 | 20 | ## Current status 21 | 22 | FileSync was first made during a lecture on AngularJS/Socket.io/NodeJS with IUT Nantes students on 3rd March 2015. 23 | 24 |

25 | 26 |

27 | 28 | ## Setup 29 | 30 | ``` 31 | npm i filesync -g 32 | filesync-server 33 | filesync-relay /path/to/directory 34 | ``` 35 | 36 | ## How to contribute 37 | 38 | - [Fork the project](https://help.github.com/articles/fork-a-repo/) 39 | - [Clone it](https://git-scm.com/docs/git-clone) on your computer 40 | - Run `npm install` inside `filesync/` 41 | - Let's do this! 42 | 43 | ## [Changelog](/CHANGELOG.md) 44 | 45 | ## Contribute / TODO 46 | 47 | See [issues](https://github.com/FGRibreau/filesync/issues) there is still a lot of things to do/improve note that **I will happily merge any pull-requests that solve each of the specified issues**. 48 | 49 | ## Bonus 50 | 51 | Since this tool is primary build for teaching, it will also display the number of students that don't currently have the page on focus using HTML5 Page Visibility API. (But yeah they can always open another browser window, in the end that feature was mainly developed for fun...) 52 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | {{ social.plop }} 16 | 17 | 18 | 19 | 20 | 21 |
22 | {{ viewer }} 23 |
24 | 25 |
26 |
27 | {{ message }} 28 |
29 |
30 |
31 | 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 |

File History {{ history.visibility.states }}

43 | 44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 | {{ edit.timestamp | date : format : shortTime }} 53 |
54 | 55 |
56 | {{ edit.filename }} 57 |
58 | 59 |
60 | x 61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 |
77 | ... aucun historique... mais que fait le prof ! 78 |
79 |
80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var io = require('socket.io'); 4 | var express = require('express'); 5 | var path = require('path'); 6 | var app = express(); 7 | var _ = require('lodash'); 8 | 9 | var logger = require('winston'); 10 | var config = require('./config')(logger); 11 | 12 | app.use(express.static(path.resolve(__dirname, './public'))); 13 | 14 | app.get('/', function(req, res) { 15 | res.sendFile(__dirname + '/public/index.html'); 16 | }); 17 | 18 | var server = app.listen(config.server.port, function() { 19 | logger.info('Server listening on %s', config.server.port); 20 | }); 21 | 22 | 23 | var sio = io(server); 24 | 25 | sio.set('authorization', function(handshakeData, accept) { 26 | // @todo use something else than a private `query` 27 | handshakeData.isAdmin = handshakeData._query.access_token === config.auth.token; 28 | accept(null, true); 29 | }); 30 | 31 | function Viewers(sio) { 32 | var data = []; 33 | 34 | function notifyChanges() { 35 | sio.emit('viewers:updated', data); 36 | } 37 | 38 | return { 39 | add: function add(nickname) { 40 | data.push(nickname); 41 | notifyChanges(); 42 | }, 43 | remove: function remove(nickname) { 44 | var idx = data.indexOf(nickname); 45 | if (idx > -1) { 46 | data.splice(idx, 1); 47 | } 48 | notifyChanges(); 49 | console.log('-->', data); 50 | } 51 | }; 52 | } 53 | 54 | var viewers = Viewers(sio); 55 | 56 | function FileHistory(options) { 57 | var maxSize = options.maxSize || 10; 58 | /** 59 | * filename: [basename, updatedAt, content] 60 | * @type {Object} 61 | */ 62 | 63 | var history = {}; 64 | 65 | this.length = function() { 66 | return Object.keys(history); 67 | }; 68 | 69 | this.add = function(basename, updatedAt, content) { 70 | history[basename] = [basename, updatedAt, content]; 71 | 72 | if (this.length >= maxSize) { 73 | var keyToRemove = _.chain(history) 74 | .sortBy(1) // sort by updatedAt 75 | .first() // get the oldest entry in history 76 | .first() // get "basename" 77 | .value(); 78 | 79 | console.log(keyToRemove); 80 | delete history[keyToRemove]; 81 | } 82 | 83 | console.log(history); 84 | }; 85 | 86 | this.forEach = function(f) { 87 | _.values(history).forEach(f); 88 | }; 89 | 90 | } 91 | 92 | var history = new FileHistory({ 93 | maxSize: 10 94 | }); 95 | 96 | function notifyFileChanged(basename, updatedAt, content) { 97 | // forward the event to everyone 98 | sio.emit('file:changed', basename, updatedAt, content); 99 | } 100 | 101 | // @todo extract in its own 102 | sio.on('connection', function(socket) { 103 | 104 | socket.on('message', function(message){ 105 | sio.emit('message', message); 106 | }); 107 | 108 | // console.log('nouvelle connexion', socket.id); 109 | socket.on('viewer:new', function(nickname) { 110 | socket.nickname = nickname; 111 | viewers.add(nickname); 112 | console.log('new viewer with nickname %s', nickname, viewers); 113 | }); 114 | 115 | // nouveau 116 | // var i = 0; 117 | // setInterval(function(){ 118 | // socket.emit('message', 'hello world ' + (i++)); 119 | // }, 1000); 120 | // /nouveau 121 | 122 | socket.on('fg', function(a, b){ 123 | console.log(a, b); 124 | }) 125 | 126 | socket.on('disconnect', function() { 127 | viewers.remove(socket.nickname); 128 | console.log('viewer disconnected %s\nremaining:', socket.nickname, viewers); 129 | }); 130 | 131 | socket.on('file:changed', function(basename, updatedAt, content) { 132 | if (!socket.conn.request.isAdmin) { 133 | // if the user is not admin 134 | // skip this 135 | return socket.emit('error:auth', 'Unauthorized :)'); 136 | } 137 | 138 | history.add(basename, updatedAt, content); 139 | notifyFileChanged(basename, updatedAt, content); 140 | }); 141 | 142 | // replay history to the newly connected user 143 | history.forEach(notifyFileChanged); 144 | 145 | socket.visibility = 'visible'; 146 | 147 | socket.on('user-visibility:changed', function(state) { 148 | socket.visibility = state; 149 | sio.emit('users:visibility-states', getVisibilityCounts()); 150 | }); 151 | }); 152 | 153 | function getVisibilityCounts() { 154 | return _.chain(sio.sockets.sockets).values().countBy('visibility').value(); 155 | } 156 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : true, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 13 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 14 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 2, // {int} Number of spaces to use for indentation 16 | "latedef" : false, // true: Require variables/functions to be defined before being used 17 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` & `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // true: Require all defined variables be used 30 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 31 | "maxparams" : 10, // {int} Max number of formal params allowed per function 32 | "maxdepth" : 5, // {int} Max depth of nested blocks (within functions) 33 | "maxstatements" : 30, // {int} Max number statements per function 34 | "maxcomplexity" : 12, // {int} Max cyclomatic complexity per function 35 | "maxlen" : 110, // {int} Max number of characters per line 36 | 37 | // Relaxing 38 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 39 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 40 | "debug" : true, // true: Allow debugger statements e.g. browser breakpoints. 41 | "eqnull" : false, // true: Tolerate use of `== null` 42 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 43 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 44 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 45 | // (ex: `for each`, multiple try/catch, function expression…) 46 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 47 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 48 | "funcscope" : false, // true: Tolerate defining variables inside control statements 49 | "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') 50 | "iterator" : false, // true: Tolerate using the `__iterator__` property 51 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 52 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 53 | "laxcomma" : true, // true: Tolerate comma-first style coding 54 | "loopfunc" : false, // true: Tolerate functions being defined in loops 55 | "multistr" : false, // true: Tolerate multi-line strings 56 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 57 | "notypeof" : false, // true: Tolerate invalid typeof operator values 58 | "proto" : false, // true: Tolerate using the `__proto__` property 59 | "scripturl" : false, // true: Tolerate script-targeted URLs 60 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 61 | "sub" : true, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 62 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 63 | "validthis" : false, // true: Tolerate using this in a non-constructor function 64 | 65 | // Environments 66 | "browser" : true, // Web Browser (window, document, etc) 67 | "browserify" : false, // Browserify (node.js code in the browser) 68 | "couch" : false, // CouchDB 69 | "devel" : true, // Development/debugging (alert, confirm, etc) 70 | "dojo" : false, // Dojo Toolkit 71 | "jasmine" : false, // Jasmine 72 | "jquery" : false, // jQuery 73 | "mocha" : true, // Mocha 74 | "mootools" : false, // MooTools 75 | "node" : true, // Node.js 76 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 77 | "prototypejs" : false, // Prototype and Scriptaculous 78 | "qunit" : true, // QUnit 79 | "rhino" : false, // Rhino 80 | "shelljs" : false, // ShellJS 81 | "worker" : false, // Web Workers 82 | "wsh" : false, // Windows Scripting Host 83 | "yui" : false, // Yahoo User Interface 84 | 85 | // Custom Globals 86 | "globals" : { 87 | "angular": true 88 | } // additional predefined global variables 89 | } 90 | --------------------------------------------------------------------------------