From 09a654213e21bd2dc2b9ad92cb1569c25e2e356e Mon Sep 17 00:00:00 2001
From: Jennifer Lindgren <jennifer.lindgren93@gmail.com>
Date: Mon, 27 May 2019 09:57:33 +0200
Subject: [PATCH] Backend: Create and update project files locally when someone
 is viewing a project to allow for faster syntax validation.

---
 backend/19/Folder2/nested.py  |   1 +
 backend/19/app.py             |  14 +
 backend/19/database_helper.py | 441 +++++++++++++++++++++++++
 backend/19/napp.py            |   0
 backend/2/Folder/app.py       |   1 +
 backend/2/Folder/heh.py       |   0
 backend/2/Folder/hello.py     |   0
 backend/2/app.py              | 592 ++++++++++++++++++++++++++++++++++
 backend/2/database_helper.py  | 439 +++++++++++++++++++++++++
 backend/2/socketio_helper.py  | 301 +++++++++++++++++
 backend/2/test.py             |   1 +
 backend/Pipfile               |   1 +
 backend/Pipfile.lock          |  33 +-
 backend/app.py                |  86 +++--
 backend/db.sqlite             | Bin 282624 -> 282624 bytes
 backend/socketio_helper.py    |  39 ++-
 16 files changed, 1915 insertions(+), 34 deletions(-)
 create mode 100644 backend/19/Folder2/nested.py
 create mode 100644 backend/19/app.py
 create mode 100644 backend/19/database_helper.py
 create mode 100644 backend/19/napp.py
 create mode 100644 backend/2/Folder/app.py
 create mode 100644 backend/2/Folder/heh.py
 create mode 100644 backend/2/Folder/hello.py
 create mode 100644 backend/2/app.py
 create mode 100644 backend/2/database_helper.py
 create mode 100644 backend/2/socketio_helper.py
 create mode 100644 backend/2/test.py

diff --git a/backend/19/Folder2/nested.py b/backend/19/Folder2/nested.py
new file mode 100644
index 00000000..7ddfc8f8
--- /dev/null
+++ b/backend/19/Folder2/nested.py
@@ -0,0 +1 @@
+print('hello world!')
\ No newline at end of file
diff --git a/backend/19/app.py b/backend/19/app.py
new file mode 100644
index 00000000..f579e6f8
--- /dev/null
+++ b/backend/19/app.py
@@ -0,0 +1,14 @@
+""" Doc string """
+
+print('Hello world!')
+
+for i in range(0, 1):
+    print('Hello world!')
+print('Hello!')
+
+print('Hello!')
+
+for i in range(0, 1):
+    test_var = '10'
+
+TST = 2
diff --git a/backend/19/database_helper.py b/backend/19/database_helper.py
new file mode 100644
index 00000000..023d63f9
--- /dev/null
+++ b/backend/19/database_helper.py
@@ -0,0 +1,441 @@
+import os
+
+from __init__ import app
+
+import json
+from datetime import datetime
+from flask import Flask, request, jsonify
+from flask_sqlalchemy import SQLAlchemy
+from flask_marshmallow import Marshmallow, fields
+
+basedir = os.path.abspath(os.path.dirname(__file__))
+
+# Database
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'db.sqlite')
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+
+db = SQLAlchemy(app)
+ma = Marshmallow(app)
+
+# EditorSettings
+class EditorSettings(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    fontSize = db.Column(db.Integer, nullable=False)
+    margin = db.Column(db.Integer, nullable=False)
+    tabSize = db.Column(db.Integer, nullable=False)
+
+    def __init__(self, font_size, margin, tab_size):
+        self.fontSize = font_size
+        self.margin = margin
+        self.tabSize = tab_size
+
+class EditorSettingsSchema(ma.Schema):
+    class Meta:
+        model = EditorSettings
+        fields = ('id', 'fontSize', 'margin', 'tabSize')
+
+editor_settings_schema = EditorSettingsSchema(strict=True)
+
+# User
+class User(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    email = db.Column(db.String(100), unique=True, nullable=False)
+    username = db.Column(db.String(50), unique=True, nullable=False)
+    password = db.Column(db.Text, nullable=False)
+    editorSettingsId = db.Column(db.Integer, db.ForeignKey(EditorSettings.id))
+    editorSettings = db.relationship("EditorSettings")
+
+    def __init__(self, email, username, password):
+        self.email = email
+        self.username = username
+        self.password = password
+
+class UserSchema(ma.Schema):
+    editorSettings = ma.Nested(EditorSettingsSchema)
+    class Meta:
+        model = User
+        fields = ('id', 'email', 'username', 'editorSettingsId', 'editorSettings')
+
+user_schema = UserSchema(strict=True)
+users_schema = UserSchema(many=True, strict=True)
+
+# Project
+class Project(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    title = db.Column(db.String(50), nullable=False)
+    creatorId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    creator = db.relationship('User')
+    created = db.Column(db.String(50), default=datetime.now, nullable=False)
+    edited = db.Column(db.String(50), onupdate=datetime.now)
+    archived = db.Column(db.Boolean, default=False, nullable=False)
+
+    def __init__(self, title, creator_id, created=None, edited=None, archived=False):
+        self.title = title
+        self.creatorId = creator_id
+        self.created = created
+        self.edited = edited
+        self.archived = archived
+
+class ProjectSchema(ma.Schema):
+    creator = ma.Nested(UserSchema)
+    class Meta:
+        model = Project
+        fields = ('id', 'title', 'creatorId', 'creator', 'created', 'edited', 'archived')
+
+project_schema = ProjectSchema(strict=True)
+projects_schema = ProjectSchema(many=True, strict=True)
+
+# File
+class File(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False)
+    project = db.relationship('Project', lazy='subquery')
+    name = db.Column(db.String(100), nullable=False)
+    isFolder = db.Column(db.Boolean, nullable=False)
+    parent = db.Column(db.String(100))
+    content = db.Column(db.Text)
+
+    def __init__(self, project_id, name, is_folder, parent, content):
+        self.projectId = project_id
+        self.name = name
+        self.isFolder = is_folder
+        self.parent = parent
+        self.content = content
+
+class FileSchema(ma.Schema):
+    project = ma.Nested(ProjectSchema)
+    class Meta:
+        model = File
+        fields = ('id', 'projectId', 'project', 'name', 'isFolder', 'parent', 'content')
+
+file_schema = FileSchema(strict=True)
+files_schema = FileSchema(many=True, strict=True)
+
+# Collaborator
+class Collaborator(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False)
+    project = db.relationship('Project', lazy='subquery')
+    userId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    user = db.relationship('User', lazy='subquery')
+    permission = db.Column(db.Text, nullable=False)
+
+    def __init__(self, project_id, user_id, permission):
+        self.projectId = project_id
+        self.userId = user_id
+        self.permission = permission
+
+class CollaboratorSchema(ma.Schema):
+    project = ma.Nested(ProjectSchema)
+    user = ma.Nested(UserSchema)
+    class Meta:
+        fields = ('id', 'projectId', 'project', 'userId', 'user', 'permission')
+
+collaborator_schema = CollaboratorSchema(strict=True)
+collaborators_schema = CollaboratorSchema(many=True, strict=True)
+
+# Message
+class Message(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False)
+    project = db.relationship('Project')
+    authorId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    author = db.relationship('User')
+    message = db.Column(db.Text)
+    time = db.Column(db.String(50), default=datetime.now, nullable=False)
+
+    def __init__(self, project_id, author_id, message, time=None):
+        self.projectId = project_id
+        self.authorId = author_id
+        self.message = message
+        self.time = time
+
+class MessageSchema(ma.Schema):
+    project = ma.Nested(ProjectSchema)
+    author = ma.Nested(UserSchema)
+    class Meta:
+        fields = ('id', 'projectId', 'project', 'authorId', 'author', 'message', 'time')
+
+message_schema = MessageSchema(strict=True)
+messages_schema = MessageSchema(many=True, strict=True)
+
+### USER ###
+def add_user(new_user):
+    db.session.add(new_user)
+    db.session.commit()
+
+def get_users():
+    all_users = User.query.all()
+    result = users_schema.dump(all_users)
+    return result.data
+
+def get_user(id):
+    user = User.query.get(id)
+    return user
+
+def update_user(id, updated_user):
+    user = User.query.get(id)
+    user.email = updated_user.email
+    user.username = updated_user.username
+    user.password = updated_user.password
+    user.editorSettingsId = updated_user.editorSettingsId
+    db.session.commit()
+    return user
+
+def delete_user(id):
+    user = User.query.get(id)
+    db.session.delete(user)
+    db.session.commit()
+    return user
+
+def delete_all_users():
+    num_users_deleted = db.session.query(User).delete()
+    db.session.commit()
+    return num_users_deleted
+
+def is_user(user):
+    return user != None and 'id' in user.keys() and bool(User.query.get(user['id']))
+
+def is_user_id(userId):
+    return userId != None and bool(User.query.get(userId))
+
+def is_unregistered_email(email):
+    return not bool(get_user_from_email(email))
+
+def is_unregistered_username(username):
+    return not bool(get_user_from_username(username))
+
+def get_user_from_email(email):
+    return User.query.filter_by(email=email).first()
+
+def get_user_from_username(username):
+    return User.query.filter_by(username=username).first()
+
+def get_username_from_email(email):
+    return get_user_from_email(email).username
+
+def is_valid_email_password(email, password):
+    user = get_user_from_email(email)
+    valid_password = user.password if user else None
+    return password != None and password == valid_password
+
+def is_valid_username_password(username, password):
+    user = get_user_from_username(username)
+    valid_password = user.password if user else None
+    return password != None and password == valid_password
+
+def change_password(username, newPassword):
+    user = get_user_from_username(username)
+    user.password = newPassword
+    db.session.commit()
+    return user
+
+### EDITORSETTINGS ###
+def add_editor_settings(editor_settings):
+    db.session.add(editor_settings)
+    db.session.commit()
+
+def get_editor_settings(user_id):
+    return EditorSettings.query.filter_by(userId=user_id).first()
+
+def update_editor_settings(updated_editor_settings):
+    editor_settings = EditorSettings.query.get(updated_editor_settings.id)
+    editor_settings.fontSize = updated_editor_settings.fontSize
+    editor_settings.margin = updated_editor_settings.margin
+    editor_settings.tabSize = updated_editor_settings.tabSize
+    db.session.commit()
+    return editor_settings
+
+def delete_editor_settings(id):
+    editor_settings = EditorSettings.query.get(id)
+    db.session.delete(editor_settings)
+    db.session.commit()
+    return editor_settings
+
+def delete_all_editor_settings():
+    num_editor_settings_deleted = db.session.query(EditorSettings).delete()
+    db.session.commit()
+    return num_editor_settings_deleted
+
+### PROJECT ###
+def add_project(project):
+    db.session.add(project)
+    db.session.commit()
+
+def get_projects():
+    all_projects = Project.query.all()
+    result = projects_schema.dump(all_projects)
+    return result.data
+
+def get_user_projects(user_id):
+    """ Returns all projects where user is the creator or a collaborator """
+    user_projects = Project.query.filter_by(creatorId=user_id).all()
+    collaborator_project_ids = [id for id, in db.session.query(Collaborator.projectId).filter_by(userId=user_id)]
+    user_projects += Project.query.filter(Project.id.in_(collaborator_project_ids)).all()
+    result = projects_schema.dump(user_projects)
+    return result.data
+
+def get_project(id):
+    project = Project.query.get(id)
+    return project
+
+def update_project(updated_project):
+    project = Project.query.get(updated_project.id)
+    project.title = updated_project.title
+    project.creatorId = updated_project.creatorId
+    project.archived = updated_project.archived
+    db.session.commit()
+    return project
+
+def delete_project(id):
+    project = Project.query.get(id)
+    db.session.delete(project)
+    db.session.commit()
+    return project
+
+def delete_user_projects(user_id):
+    """ Deletes all projects owned by user with id. """
+    user = User.query.get(user_id)
+    user_projects_query = Project.query.filter_by(creatorId=user_id)
+
+    user_projects = user_projects_query.all()
+    for project in user_projects:
+        delete_project_files(project.id)
+        delete_project_collaborators(project.id)
+        delete_project_messages(project.id)
+        delete_project(project.id)
+
+    num_projects_deleted = user_projects_query.delete()
+    db.session.commit()
+    return num_projects_deleted
+
+def delete_all_projects():
+    num_projects_deleted = db.session.query(Project).delete()
+    db.session.commit()
+    return num_projects_deleted
+
+### FILE ###
+def add_file(file):
+    db.session.add(file)
+    db.session.commit()
+
+def get_file(id):
+    return File.query.get(id)
+
+def get_files():
+    all_files = File.query.all()
+    result = files_schema.dump(all_files)
+    return result.data
+
+def get_project_files(project_id):
+    project_files = File.query.filter_by(projectId=project_id).all()
+    result = files_schema.dump(project_files)
+    return result.data
+
+def update_file(updated_file):
+    file = File.query.get(updated_file.id)
+    file.projectId = updated_file.projectId
+    file.name = updated_file.name
+    file.isFolder = updated_file.isFolder
+    file.parent = updated_file.parent
+    file.content = updated_file.content
+    db.session.commit()
+    return file
+
+def delete_file(file_id):
+    file = File.query.filter_by(id=file_id).first()
+    db.session.delete(file)
+    db.session.commit()
+    return file
+
+def delete_project_files(project_id):
+    num_project_files_deleted = File.query.filter_by(projectId=project_id).delete()
+    db.session.commit()
+    return num_project_files_deleted
+
+def delete_all_files():
+    num_files_deleted = db.session.query(File).delete()
+    db.session.commit()
+    return num_files_deleted
+
+### COLLABORATOR ###
+def add_collaborator(collaborator):
+    db.session.add(collaborator)
+    db.session.commit()
+
+def get_collaborator(id):
+    collaborator = Collaborator.query.get(id)
+    return collaborator
+
+def get_collaborators():
+    all_collaborators = Collaborator.query.all()
+    result = collaborators_schema.dump(all_collaborators)
+    return result.data
+
+def get_project_collaborators(project_id):
+    project_collaborators = Collaborator.query.filter_by(projectId=project_id).all()
+    result = collaborators_schema.dump(project_collaborators)
+    return result.data
+
+def update_collaborator(updated_collaborator):
+    collaborator = Collaborator.query.get(updated_collaborator.id)
+    collaborator.projectId = updated_collaborator.projectId
+    collaborator.userId = updated_collaborator.userId
+    collaborator.permission = updated_collaborator.permission
+    db.session.commit()
+    return collaborator
+
+def delete_collaborator(collaborator_id):
+    collaborator = Collaborator.query.filter_by(id=collaborator_id).first()
+    db.session.delete(collaborator)
+    db.session.commit()
+    return collaborator
+
+def delete_project_collaborators(project_id):
+    num_project_collaborators_deleted = Collaborator.query.filter_by(projectId=project_id).delete()
+    db.session.commit()
+    return num_project_collaborators_deleted
+
+def delete_all_collaborators():
+    num_collaborators_deleted = db.session.query(Collaborator).delete()
+    db.session.commit()
+    return num_collaborators_deleted
+
+### Message ###
+def add_message(message):
+    db.session.add(message)
+    db.session.commit()
+
+def get_message(id):
+    message = Message.query.get(id)
+    return message
+
+def get_messages():
+    all_messages = Message.query.all()
+    result = messages_schema.dump(all_messages)
+    return result.data
+
+def get_project_messages(project_id):
+    project_messages = Message.query.filter_by(projectId=project_id).all()
+    result = messages_schema.dump(project_messages)
+    return result.data
+
+def update_message(updated_message):
+    message = Message.query.get(updated_message.id)
+    message.projectId = updated_message.projectId
+    message.autorId = updated_message.autorId
+    message.time = updated_message.time
+    db.session.commit()
+    return message
+
+def delete_project_messages(project_id):
+    num_project_messages_deleted = Message.query.filter_by(projectId=project_id).delete()
+    db.session.commit()
+    return num_project_messages_deleted
+
+def delete_all_messages():
+    num_messages_deleted = db.session.query(Message).delete()
+    db.session.commit()
+    return num_messages_deleted
+
+print('Hello world!')
\ No newline at end of file
diff --git a/backend/19/napp.py b/backend/19/napp.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/2/Folder/app.py b/backend/2/Folder/app.py
new file mode 100644
index 00000000..f9204ee5
--- /dev/null
+++ b/backend/2/Folder/app.py
@@ -0,0 +1 @@
+print('Hello!')
\ No newline at end of file
diff --git a/backend/2/Folder/heh.py b/backend/2/Folder/heh.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/2/Folder/hello.py b/backend/2/Folder/hello.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/2/app.py b/backend/2/app.py
new file mode 100644
index 00000000..63fe4765
--- /dev/null
+++ b/backend/2/app.py
@@ -0,0 +1,592 @@
+import os, sys, subprocess, shutil
+import random
+
+import logging
+from subprocess import Popen, PIPE
+from datetime import datetime
+from flask import Flask, request, jsonify, Response, json
+from flask_jwt_extended import JWTManager
+from flask_jwt_extended import (create_access_token, create_refresh_token,
+                                jwt_required, jwt_refresh_token_required, get_jwt_identity)
+from flask_bcrypt import Bcrypt
+from flask_socketio import emit
+from base64 import b64decode
+from simpleflock import SimpleFlock
+
+from __init__ import app
+import database_helper as db
+from socketio_helper import socketio, SIMPLE_FLOCK_TIMEOUT
+
+flask_bcrypt = Bcrypt(app)
+jwt = JWTManager(app)
+
+# Http status codes
+OK_STATUS_CODE = 200
+BAD_REQUEST_STATUS_CODE = 400
+CONFLICT_STATUS_CODE = 409
+UNAUTHORIZED_STATUS_CODE = 401
+
+DEFAULT_EDITOR_SETTINGS = {'fontSize': 16, 'margin': 80, 'tabSize': 4}
+
+### GENERAL ###
+def generate_response(response, status, headers={}):
+    response.status_code = status
+    response.headers = {**{
+        'Content-Type': 'application/json',
+        'Access-Control-Expose-Headers' : 'X-Auth-Token',
+        }, **headers}
+    return response
+
+### Authentication ###
+def is_valid_credentials(usernameEmail, password):
+    if '@' in usernameEmail:
+        user = db.get_user_from_email(usernameEmail)
+    else:
+        user = db.get_user_from_username(usernameEmail)
+    return user and flask_bcrypt.check_password_hash(user.password,
+        b64decode(password).decode("utf-8"))
+
+def is_valid_password(password):
+    return len(password) >= 8 and len(password) <= 100
+
+def is_valid_collaborators(collaborators):
+    if not collaborators:
+        return True
+    acceptedPermissions = ['View-only', 'May edit']
+    for collaborator in collaborators:
+        if (not collaborator or not db.is_user_id(collaborator['userId']) or
+        not collaborator['permission'] in acceptedPermissions):
+            return False
+    return True
+
+@jwt.unauthorized_loader
+def unauthorized_response(callback):
+    return jsonify({
+        'ok': False,
+        'message': 'Missing Authorization Header'
+    }), 401
+
+@app.route('/api/auth', methods=["GET"])
+@jwt_required
+def auth():
+    return generate_response(jsonify({
+        'success': True,
+        'message': 'Authorization successsful'
+        }), OK_STATUS_CODE)
+
+@app.route('/api/refresh', methods=['POST'])
+@jwt_refresh_token_required
+def refresh():
+    current_user = get_jwt_identity()
+    return generate_response(jsonify({
+        'success': True,
+        'message': 'Successfully refreshed token',
+        'jwt': create_access_token(identity=current_user, fresh=False)
+        }), OK_STATUS_CODE)
+
+### USER ###
+@app.route('/api/sign_up', methods=['POST'])
+def sign_up():
+    email = request.json['email']    
+    username = request.json['username']
+    password = b64decode(request.json['password']).decode("utf-8")
+    if (not email or not username or not password):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'Bad request.'
+        }), BAD_REQUEST_STATUS_CODE)
+    elif (not db.is_unregistered_email(email.lower())):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'User with that email already exists'
+        }), CONFLICT_STATUS_CODE)
+    elif (not db.is_unregistered_username(username.lower())):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'User with that username already exists'
+        }), CONFLICT_STATUS_CODE)
+    elif (not is_valid_password(password)):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'Password does not fulfill requirements'
+        }), BAD_REQUEST_STATUS_CODE)
+    else:
+        new_user = db.User(
+            email.lower(),
+            username.lower(),
+            flask_bcrypt.generate_password_hash(password).decode('utf-8'))
+        db.add_user(new_user)
+
+        new_editor_settings = db.EditorSettings(
+            DEFAULT_EDITOR_SETTINGS['fontSize'],
+            DEFAULT_EDITOR_SETTINGS['margin'],
+            DEFAULT_EDITOR_SETTINGS['tabSize'])
+        db.add_editor_settings(new_editor_settings)
+
+        new_user.editorSettingsId = new_editor_settings.id
+        db.update_user(new_user.id, new_user)
+        return generate_response(db.user_schema.jsonify(new_user),
+            OK_STATUS_CODE)
+
+@app.route('/api/sign_in', methods=['GET'])
+def sign_in():
+    auth = request.authorization
+    usernameEmail = auth.username
+    password = auth.password
+
+    if not usernameEmail or not password:
+        message = 'Missing credentials!'
+        status = BAD_REQUEST_STATUS_CODE
+    elif not is_valid_credentials(usernameEmail.lower(), password):
+        message = 'Incorrect username/email or password'
+        status = UNAUTHORIZED_STATUS_CODE
+    else:
+        if '@' in usernameEmail:
+            username = db.get_username_from_email(usernameEmail.lower())
+        else:
+            username = usernameEmail
+
+        db_user = db.get_user_from_username(username.lower())
+        user = {
+            'id': db_user.id,
+            'email': db_user.email,
+            'username': db_user.username,
+            'editorSettingsId': db_user.editorSettingsId,
+            'editorSettings': db.editor_settings_schema.dumps(db_user.editorSettings),
+            'jwt': create_access_token(identity=username),
+            'refreshToken': create_refresh_token(identity=username)
+        }
+        return generate_response(jsonify(user), OK_STATUS_CODE)
+
+    return generate_response(jsonify({
+        'success': False,
+        'message': message
+        }), status)
+
+@app.route('/api/change_password', methods=['POST', 'PUT'])
+@jwt_required
+def change_password():
+    username = request.json['username']
+    user = db.get_user_from_username(username)
+    oldPassword = request.json['oldPassword']
+    newPassword = request.json['newPassword']
+    if (not user):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'No user with that username'
+        }), BAD_REQUEST_STATUS_CODE)
+    elif not is_valid_credentials(username, oldPassword):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'Old password was incorrect'
+        }),UNAUTHORIZED_STATUS_CODE)
+    elif (not is_valid_password(newPassword)):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'New password does not fulfill requirements'
+        }), BAD_REQUEST_STATUS_CODE)
+    else:
+        updated_user = db.change_password(
+            username,
+            flask_bcrypt.generate_password_hash(newPassword).decode('utf-8')
+            )
+        return generate_response(db.user_schema.jsonify(updated_user), OK_STATUS_CODE)
+
+@app.route('/api/delete_account', methods=['DELETE'])
+def delete_user():
+    auth = request.authorization
+    username = auth.username
+    password = auth.password
+    user = db.get_user_from_username(username)
+    if not is_valid_credentials(username, password):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'Password was incorrect'
+        }),UNAUTHORIZED_STATUS_CODE)
+    elif (not user):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'No user with that username'
+        }), BAD_REQUEST_STATUS_CODE)
+    else:
+        db.delete_user_projects(user.id)
+        db.delete_editor_settings(user.editorSettingsId)
+        db.delete_user(user.id)
+        return generate_response(jsonify({
+            'success': True,
+            'message': 'Successfully deleted account'
+        }), OK_STATUS_CODE)
+
+@app.route('/api/get_users', methods=['GET'])
+@jwt_required
+def get_users_request():
+    return generate_response(db.users_schema.jsonify(db.get_users()),
+        OK_STATUS_CODE)
+
+@app.route('/api/get_user/<id>', methods=['GET'])
+@jwt_required
+def get_user_request(id):
+    return generate_response(db.user_schema.jsonify(db.get_user(id)),
+        OK_STATUS_CODE)
+
+### EDITOR SETTINGS ###
+@app.route('/api/update_editor_settings/<user_id>', methods=['PUT'])
+@jwt_required
+def update_editor_settings_request(user_id):
+    editor_settings = request.json['editorSettings']
+    updated_editor_settings = db.EditorSettings(
+        editor_settings['fontSize'],
+        editor_settings['margin'],
+        editor_settings['tabSize'],
+    )
+    updated_editor_settings.id = editor_settings['id']
+    return generate_response(db.editor_settings_schema.jsonify(
+        db.update_editor_settings(updated_editor_settings)), OK_STATUS_CODE)
+
+### PROJECT ###
+@app.route('/api/add_project', methods=['POST'])
+@jwt_required
+def new_project():
+    title = request.json['title']
+    creatorId = request.json['creatorId']
+    collaborators = request.json['collaborators'] if request.json['collaborators'] != [] else None
+    if (not title or not creatorId):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'Bad request.'
+        }), BAD_REQUEST_STATUS_CODE)
+    elif (not db.is_user_id(creatorId)):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'User does not exist'
+        }), BAD_REQUEST_STATUS_CODE)
+    elif (not is_valid_collaborators(collaborators)):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'Invalid collaborators'
+        }), BAD_REQUEST_STATUS_CODE)
+    else:
+        new_project = db.Project(title, creatorId)
+        db.add_project(new_project)
+
+        if collaborators:
+            for collaborator in collaborators:
+                new_collaborator = db.Collaborator(
+                    project_id=new_project.id,
+                    user_id=collaborator['userId'],
+                    permission=collaborator['permission'])
+                db.add_collaborator(new_collaborator)
+        
+        
+        return generate_response(db.project_schema.jsonify(new_project), OK_STATUS_CODE)
+
+@app.route('/api/duplicate_project/<project_id>', methods=['POST'])
+@jwt_required
+def duplicate_project(project_id):
+    title = request.json['title']
+    creator_id = request.json['creatorId']
+    if (not title or not creator_id):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'Bad request.'
+        }), BAD_REQUEST_STATUS_CODE)
+    elif (not db.is_user_id(creator_id)):
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'User does not exist'
+        }), BAD_REQUEST_STATUS_CODE)
+    else:
+        old_project = db.get_project(project_id)
+        duplicated_project = db.Project(
+            title,
+            creator_id,
+            archived=old_project.archived
+        )
+        db.add_project(duplicated_project)
+
+        collaborators = db.get_project_collaborators(project_id)
+        if collaborators:
+            for collaborator in collaborators:
+                if collaborator['userId'] != creator_id:
+                    duplicated_collaborator = db.Collaborator(
+                        project_id=duplicated_project.id,
+                        user_id=collaborator['userId'],
+                        permission=collaborator['permission'])
+                    db.add_collaborator(duplicated_collaborator)
+
+        if old_project.creatorId != creator_id:
+            old_creator_collaborator = db.Collaborator(
+                project_id=duplicated_project.id,
+                user_id=old_project.creatorId,
+                permission='May edit')
+            db.add_collaborator(old_creator_collaborator)
+
+        files = db.get_project_files(project_id)
+        if files:
+            for file in files:
+                duplicated_file = db.File(
+                    project_id=duplicated_project.id,
+                    name=file['name'],
+                    is_folder=file['isFolder'],
+                    parent=file['parent'],
+                    content=file['content'])
+                db.add_file(duplicated_file)
+
+        return generate_response(db.project_schema.jsonify(duplicated_project), OK_STATUS_CODE)
+
+@app.route('/api/get_projects', methods=['GET'])
+@jwt_required
+def get_projects_request():
+    return generate_response(db.projects_schema.jsonify(db.get_projects()),
+        OK_STATUS_CODE)
+
+@app.route('/api/get_projects/<user_id>', methods=['GET'])
+@jwt_required
+def get_user_projects_request(user_id):
+    return generate_response(db.projects_schema.jsonify(db.get_user_projects(user_id)),
+        OK_STATUS_CODE)
+
+@app.route('/api/get_project/<id>', methods=['GET'])
+@jwt_required
+def get_project_request(id):
+    return generate_response(db.project_schema.jsonify(db.get_project(id)),
+        OK_STATUS_CODE)
+
+@app.route('/api/update_project/<id>', methods=['POST', 'PUT'])
+@jwt_required
+def update_project_request(id):
+    project = json.loads(request.json['project'])
+    updated_project = db.Project(
+        title = project['title'],
+        creator_id = project['creatorId'],
+        archived = project['archived'])
+    updated_project.id = project['id']
+
+    return generate_response(db.project_schema.jsonify(
+        db.update_project(updated_project)), OK_STATUS_CODE)
+
+@app.route('/api/set_archived_project/<project_id>', methods=['POST', 'PUT'])
+@jwt_required
+def set_archived_project_request(project_id):
+    project = db.get_project(project_id)
+    project.archived = request.json['archived']
+
+    return generate_response(db.project_schema.jsonify(
+        db.update_project(project)), OK_STATUS_CODE)
+
+@app.route('/api/delete_project/<project_id>', methods=['DELETE'])
+@jwt_required
+def delete_project_request(project_id):
+    # TODO: Check that the user trying to delete the project is the creator!
+    db.delete_project_files(project_id)
+    db.delete_project_collaborators(project_id)
+    db.delete_project_messages(project_id)
+    db.delete_project(project_id)
+    return generate_response(jsonify({
+            'success': True,
+            'message': 'Successfully deleted project'
+        }), OK_STATUS_CODE)
+
+@app.route('/api/parse_project_files/<project_id>', methods=['GET'])
+@jwt_required
+def parse_file_request(project_id):
+    if db.get_project(project_id):
+        if os.path.exists(str(project_id)):
+            find_cmd = 'find ' + str(project_id) + '/ -type f -name "*.py"'
+            find = Popen(find_cmd, shell=True, stdout=PIPE)
+            files = find.communicate()[0].decode('utf-8')
+            files = ' '.join(files.splitlines())
+            
+            pylint_cmd = 'pylint --output-format=json ' + files
+            pylint = Popen(pylint_cmd, shell=True, stdout=PIPE)
+            result = pylint.communicate()[0].decode('utf-8')
+        else:
+            result = '[]'
+        return generate_response(jsonify({
+            'success': True,
+            'message': 'Successfully parsed files.',
+            'result': result
+        }), OK_STATUS_CODE)
+    else:
+        return generate_response(jsonify({
+            'success': False,
+            'message': 'No such project'
+        }), BAD_REQUEST_STATUS_CODE)
+
+### FILE ###
+@app.route('/api/create_file', methods=['POST'])
+@jwt_required
+def create_file_request():
+    file = request.json['file']
+    new_file = db.File(
+        file['projectId'],
+        file['name'],
+        file['isFolder'],
+        file['parent'],
+        file['content'],
+    )
+    db.add_file(new_file)
+
+    # Update edited date in project
+    project = db.get_project(new_file.projectId)
+    project.edited = datetime.now()
+    db.update_project(project)
+
+    # Add file locally on server if project is open
+    open_project_path = str(new_file.projectId) + '/'
+    if os.path.exists(open_project_path):
+        if new_file.parent:
+            file_path = open_project_path + new_file.parent + '/' + new_file.name
+        else:
+            file_path = open_project_path + new_file.name
+
+        if new_file.isFolder:
+            os.makedirs(file_path)
+        else:
+            f = open(file_path, 'w+')
+            f.write(new_file.content)
+            f.close()
+
+    return generate_response(db.file_schema.jsonify(
+        new_file), OK_STATUS_CODE)
+
+@app.route('/api/update_file/<file_id>', methods=['PUT'])
+@jwt_required
+def update_file_request(file_id):
+    file = request.json['file']
+    old_file = db.get_file(file['id'])
+
+    updated_file = db.File(
+        file['projectId'],
+        file['name'],
+        file['isFolder'],
+        file['parent'],
+        file['content'])
+    updated_file.id = file['id']
+
+    # Update edited date in project
+    project = db.get_project(updated_file.projectId)
+    project.edited = datetime.now()
+    db.update_project(project)
+
+    # Update file locally on server if project is open
+    open_project_path = str(project.id) + '/'
+    if old_file.parent:
+        old_path = open_project_path + old_file.parent + '/' + old_file.name
+    else:
+        old_path = open_project_path + old_file.name
+    if os.path.exists(old_path):
+        with SimpleFlock(str(project.id) + 'lock', SIMPLE_FLOCK_TIMEOUT):
+            if os.path.exists(old_path):
+                if file['parent']:
+                    new_path = open_project_path + file['parent'] + '/' + file['name']
+                else:
+                    new_path = open_project_path + file['name']
+                os.rename(old_path, new_path)
+
+    return generate_response(db.file_schema.jsonify(
+        db.update_file(updated_file)), OK_STATUS_CODE)
+
+@app.route('/api/delete_file/<file_id>', methods=['DELETE'])
+@jwt_required
+def delete_file_request(file_id):
+    # Update edited date in project
+    file = db.get_file(file_id)
+    project = db.get_project(file.projectId)
+    project.edited = datetime.now()
+    db.update_project(project)
+
+    # Remove file locally on server if project is open
+    open_project_path = str(file.projectId) + '/'
+    if file.parent:
+        path = open_project_path + file.parent + '/' + file.name
+    else:
+        path = open_project_path + file.name
+    if os.path.exists(path):
+        with SimpleFlock(str(file.projectId) + 'lock', SIMPLE_FLOCK_TIMEOUT):
+            if os.path.exists(path):
+                if file.isFolder:
+                    shutil.rmtree(path)
+                else:
+                    os.remove(path)
+
+    return generate_response(db.file_schema.jsonify(
+        db.delete_file(file_id)), OK_STATUS_CODE)
+
+@app.route('/api/get_project_files/<project_id>', methods=['GET'])
+@jwt_required
+def get_project_files_request(project_id):
+    return generate_response(db.files_schema.jsonify(
+        db.get_project_files(project_id)), OK_STATUS_CODE)
+
+### COLLABORATOR ###
+@app.route('/api/add_collaborator', methods=['POST'])
+@jwt_required
+def add_collaborator_request():
+    collaborator = request.json['collaborator']
+    new_collaborator = db.Collaborator(
+        collaborator['projectId'],
+        collaborator['userId'],
+        collaborator['permission'],
+    )
+    db.add_collaborator(new_collaborator)
+
+    # Update edited date in project
+    project = db.get_project(new_collaborator.projectId)
+    project.edited = datetime.now()
+    db.update_project(project)
+
+    return generate_response(db.collaborator_schema.jsonify(
+        new_collaborator), OK_STATUS_CODE)
+
+
+@app.route('/api/update_collaborator/<collaborator_id>', methods=['PUT'])
+@jwt_required
+def update_collaborator_request(collaborator_id):
+    # Update collaborator
+    collaborator = request.json['collaborator']
+    updated_collaborator = db.Collaborator(
+        collaborator['projectId'],
+        collaborator['userId'],
+        collaborator['permission']
+        )
+    updated_collaborator.id = collaborator['id']
+
+    # Update edited date in project
+    project = db.get_project(updated_collaborator.projectId)
+    project.edited = datetime.now()
+    db.update_project(project)
+
+    return generate_response(db.collaborator_schema.jsonify(
+        db.update_collaborator(updated_collaborator)), OK_STATUS_CODE)
+
+@app.route('/api/delete_collaborator/<collaborator_id>', methods=['DELETE'])
+@jwt_required
+def delete_collaborator_request(collaborator_id):
+    # Update edited date in project
+    collaborator = db.get_collaborator(collaborator_id)
+    project = db.get_project(collaborator.projectId)
+    project.edited = datetime.now()
+    db.update_project(project)
+
+    return generate_response(db.collaborator_schema.jsonify(
+        db.delete_collaborator(collaborator_id)), OK_STATUS_CODE)
+
+@app.route('/api/get_project_collaborators/<project_id>', methods=['GET'])
+@jwt_required
+def get_project_collaborators_request(project_id):
+    return generate_response(db.collaborators_schema.jsonify(
+        db.get_project_collaborators(project_id)), OK_STATUS_CODE)
+
+### MESSAGE ###
+@app.route('/api/get_project_messages/<project_id>', methods=['GET'])
+@jwt_required
+def get_project_messages_request(project_id):
+    return generate_response(db.messages_schema.jsonify(
+        db.get_project_messages(project_id)), OK_STATUS_CODE)
+
+if __name__ == '__main__':
+    socketio.run(app, host='0.0.0.0')
+
+    """ If you deleted the db, run the following to re-initialize it. """
+    # db.db.create_all()
\ No newline at end of file
diff --git a/backend/2/database_helper.py b/backend/2/database_helper.py
new file mode 100644
index 00000000..8bfe3a7c
--- /dev/null
+++ b/backend/2/database_helper.py
@@ -0,0 +1,439 @@
+import os
+
+from __init__ import app
+
+import json
+from datetime import datetime
+from flask import Flask, request, jsonify
+from flask_sqlalchemy import SQLAlchemy
+from flask_marshmallow import Marshmallow, fields
+
+basedir = os.path.abspath(os.path.dirname(__file__))
+
+# Database
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'db.sqlite')
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+
+db = SQLAlchemy(app)
+ma = Marshmallow(app)
+
+# EditorSettings
+class EditorSettings(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    fontSize = db.Column(db.Integer, nullable=False)
+    margin = db.Column(db.Integer, nullable=False)
+    tabSize = db.Column(db.Integer, nullable=False)
+
+    def __init__(self, font_size, margin, tab_size):
+        self.fontSize = font_size
+        self.margin = margin
+        self.tabSize = tab_size
+
+class EditorSettingsSchema(ma.Schema):
+    class Meta:
+        model = EditorSettings
+        fields = ('id', 'fontSize', 'margin', 'tabSize')
+
+editor_settings_schema = EditorSettingsSchema(strict=True)
+
+# User
+class User(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    email = db.Column(db.String(100), unique=True, nullable=False)
+    username = db.Column(db.String(50), unique=True, nullable=False)
+    password = db.Column(db.Text, nullable=False)
+    editorSettingsId = db.Column(db.Integer, db.ForeignKey(EditorSettings.id))
+    editorSettings = db.relationship("EditorSettings")
+
+    def __init__(self, email, username, password):
+        self.email = email
+        self.username = username
+        self.password = password
+
+class UserSchema(ma.Schema):
+    editorSettings = ma.Nested(EditorSettingsSchema)
+    class Meta:
+        model = User
+        fields = ('id', 'email', 'username', 'editorSettingsId', 'editorSettings')
+
+user_schema = UserSchema(strict=True)
+users_schema = UserSchema(many=True, strict=True)
+
+# Project
+class Project(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    title = db.Column(db.String(50), nullable=False)
+    creatorId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    creator = db.relationship('User')
+    created = db.Column(db.String(50), default=datetime.now, nullable=False)
+    edited = db.Column(db.String(50), onupdate=datetime.now)
+    archived = db.Column(db.Boolean, default=False, nullable=False)
+
+    def __init__(self, title, creator_id, created=None, edited=None, archived=False):
+        self.title = title
+        self.creatorId = creator_id
+        self.created = created
+        self.edited = edited
+        self.archived = archived
+
+class ProjectSchema(ma.Schema):
+    creator = ma.Nested(UserSchema)
+    class Meta:
+        model = Project
+        fields = ('id', 'title', 'creatorId', 'creator', 'created', 'edited', 'archived')
+
+project_schema = ProjectSchema(strict=True)
+projects_schema = ProjectSchema(many=True, strict=True)
+
+# File
+class File(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False)
+    project = db.relationship('Project', lazy='subquery')
+    name = db.Column(db.String(100), nullable=False)
+    isFolder = db.Column(db.Boolean, nullable=False)
+    parent = db.Column(db.String(100))
+    content = db.Column(db.Text)
+
+    def __init__(self, project_id, name, is_folder, parent, content):
+        self.projectId = project_id
+        self.name = name
+        self.isFolder = is_folder
+        self.parent = parent
+        self.content = content
+
+class FileSchema(ma.Schema):
+    project = ma.Nested(ProjectSchema)
+    class Meta:
+        model = File
+        fields = ('id', 'projectId', 'project', 'name', 'isFolder', 'parent', 'content')
+
+file_schema = FileSchema(strict=True)
+files_schema = FileSchema(many=True, strict=True)
+
+# Collaborator
+class Collaborator(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False)
+    project = db.relationship('Project', lazy='subquery')
+    userId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    user = db.relationship('User', lazy='subquery')
+    permission = db.Column(db.Text, nullable=False)
+
+    def __init__(self, project_id, user_id, permission):
+        self.projectId = project_id
+        self.userId = user_id
+        self.permission = permission
+
+class CollaboratorSchema(ma.Schema):
+    project = ma.Nested(ProjectSchema)
+    user = ma.Nested(UserSchema)
+    class Meta:
+        fields = ('id', 'projectId', 'project', 'userId', 'user', 'permission')
+
+collaborator_schema = CollaboratorSchema(strict=True)
+collaborators_schema = CollaboratorSchema(many=True, strict=True)
+
+# Message
+class Message(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False)
+    project = db.relationship('Project')
+    authorId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    author = db.relationship('User')
+    message = db.Column(db.Text)
+    time = db.Column(db.String(50), default=datetime.now, nullable=False)
+
+    def __init__(self, project_id, author_id, message, time=None):
+        self.projectId = project_id
+        self.authorId = author_id
+        self.message = message
+        self.time = time
+
+class MessageSchema(ma.Schema):
+    project = ma.Nested(ProjectSchema)
+    author = ma.Nested(UserSchema)
+    class Meta:
+        fields = ('id', 'projectId', 'project', 'authorId', 'author', 'message', 'time')
+
+message_schema = MessageSchema(strict=True)
+messages_schema = MessageSchema(many=True, strict=True)
+
+### USER ###
+def add_user(new_user):
+    db.session.add(new_user)
+    db.session.commit()
+
+def get_users():
+    all_users = User.query.all()
+    result = users_schema.dump(all_users)
+    return result.data
+
+def get_user(id):
+    user = User.query.get(id)
+    return user
+
+def update_user(id, updated_user):
+    user = User.query.get(id)
+    user.email = updated_user.email
+    user.username = updated_user.username
+    user.password = updated_user.password
+    user.editorSettingsId = updated_user.editorSettingsId
+    db.session.commit()
+    return user
+
+def delete_user(id):
+    user = User.query.get(id)
+    db.session.delete(user)
+    db.session.commit()
+    return user
+
+def delete_all_users():
+    num_users_deleted = db.session.query(User).delete()
+    db.session.commit()
+    return num_users_deleted
+
+def is_user(user):
+    return user != None and 'id' in user.keys() and bool(User.query.get(user['id']))
+
+def is_user_id(userId):
+    return userId != None and bool(User.query.get(userId))
+
+def is_unregistered_email(email):
+    return not bool(get_user_from_email(email))
+
+def is_unregistered_username(username):
+    return not bool(get_user_from_username(username))
+
+def get_user_from_email(email):
+    return User.query.filter_by(email=email).first()
+
+def get_user_from_username(username):
+    return User.query.filter_by(username=username).first()
+
+def get_username_from_email(email):
+    return get_user_from_email(email).username
+
+def is_valid_email_password(email, password):
+    user = get_user_from_email(email)
+    valid_password = user.password if user else None
+    return password != None and password == valid_password
+
+def is_valid_username_password(username, password):
+    user = get_user_from_username(username)
+    valid_password = user.password if user else None
+    return password != None and password == valid_password
+
+def change_password(username, newPassword):
+    user = get_user_from_username(username)
+    user.password = newPassword
+    db.session.commit()
+    return user
+
+### EDITORSETTINGS ###
+def add_editor_settings(editor_settings):
+    db.session.add(editor_settings)
+    db.session.commit()
+
+def get_editor_settings(user_id):
+    return EditorSettings.query.filter_by(userId=user_id).first()
+
+def update_editor_settings(updated_editor_settings):
+    editor_settings = EditorSettings.query.get(updated_editor_settings.id)
+    editor_settings.fontSize = updated_editor_settings.fontSize
+    editor_settings.margin = updated_editor_settings.margin
+    editor_settings.tabSize = updated_editor_settings.tabSize
+    db.session.commit()
+    return editor_settings
+
+def delete_editor_settings(id):
+    editor_settings = EditorSettings.query.get(id)
+    db.session.delete(editor_settings)
+    db.session.commit()
+    return editor_settings
+
+def delete_all_editor_settings():
+    num_editor_settings_deleted = db.session.query(EditorSettings).delete()
+    db.session.commit()
+    return num_editor_settings_deleted
+
+### PROJECT ###
+def add_project(project):
+    db.session.add(project)
+    db.session.commit()
+
+def get_projects():
+    all_projects = Project.query.all()
+    result = projects_schema.dump(all_projects)
+    return result.data
+
+def get_user_projects(user_id):
+    """ Returns all projects where user is the creator or a collaborator """
+    user_projects = Project.query.filter_by(creatorId=user_id).all()
+    collaborator_project_ids = [id for id, in db.session.query(Collaborator.projectId).filter_by(userId=user_id)]
+    user_projects += Project.query.filter(Project.id.in_(collaborator_project_ids)).all()
+    result = projects_schema.dump(user_projects)
+    return result.data
+
+def get_project(id):
+    project = Project.query.get(id)
+    return project
+
+def update_project(updated_project):
+    project = Project.query.get(updated_project.id)
+    project.title = updated_project.title
+    project.creatorId = updated_project.creatorId
+    project.archived = updated_project.archived
+    db.session.commit()
+    return project
+
+def delete_project(id):
+    project = Project.query.get(id)
+    db.session.delete(project)
+    db.session.commit()
+    return project
+
+def delete_user_projects(user_id):
+    """ Deletes all projects owned by user with id. """
+    user = User.query.get(user_id)
+    user_projects_query = Project.query.filter_by(creatorId=user_id)
+
+    user_projects = user_projects_query.all()
+    for project in user_projects:
+        delete_project_files(project.id)
+        delete_project_collaborators(project.id)
+        delete_project_messages(project.id)
+        delete_project(project.id)
+
+    num_projects_deleted = user_projects_query.delete()
+    db.session.commit()
+    return num_projects_deleted
+
+def delete_all_projects():
+    num_projects_deleted = db.session.query(Project).delete()
+    db.session.commit()
+    return num_projects_deleted
+
+### FILE ###
+def add_file(file):
+    db.session.add(file)
+    db.session.commit()
+
+def get_file(id):
+    return File.query.get(id)
+
+def get_files():
+    all_files = File.query.all()
+    result = files_schema.dump(all_files)
+    return result.data
+
+def get_project_files(project_id):
+    project_files = File.query.filter_by(projectId=project_id).all()
+    result = files_schema.dump(project_files)
+    return result.data
+
+def update_file(updated_file):
+    file = File.query.get(updated_file.id)
+    file.projectId = updated_file.projectId
+    file.name = updated_file.name
+    file.isFolder = updated_file.isFolder
+    file.parent = updated_file.parent
+    file.content = updated_file.content
+    db.session.commit()
+    return file
+
+def delete_file(file_id):
+    file = File.query.filter_by(id=file_id).first()
+    db.session.delete(file)
+    db.session.commit()
+    return file
+
+def delete_project_files(project_id):
+    num_project_files_deleted = File.query.filter_by(projectId=project_id).delete()
+    db.session.commit()
+    return num_project_files_deleted
+
+def delete_all_files():
+    num_files_deleted = db.session.query(File).delete()
+    db.session.commit()
+    return num_files_deleted
+
+### COLLABORATOR ###
+def add_collaborator(collaborator):
+    db.session.add(collaborator)
+    db.session.commit()
+
+def get_collaborator(id):
+    collaborator = Collaborator.query.get(id)
+    return collaborator
+
+def get_collaborators():
+    all_collaborators = Collaborator.query.all()
+    result = collaborators_schema.dump(all_collaborators)
+    return result.data
+
+def get_project_collaborators(project_id):
+    project_collaborators = Collaborator.query.filter_by(projectId=project_id).all()
+    result = collaborators_schema.dump(project_collaborators)
+    return result.data
+
+def update_collaborator(updated_collaborator):
+    collaborator = Collaborator.query.get(updated_collaborator.id)
+    collaborator.projectId = updated_collaborator.projectId
+    collaborator.userId = updated_collaborator.userId
+    collaborator.permission = updated_collaborator.permission
+    db.session.commit()
+    return collaborator
+
+def delete_collaborator(collaborator_id):
+    collaborator = Collaborator.query.filter_by(id=collaborator_id).first()
+    db.session.delete(collaborator)
+    db.session.commit()
+    return collaborator
+
+def delete_project_collaborators(project_id):
+    num_project_collaborators_deleted = Collaborator.query.filter_by(projectId=project_id).delete()
+    db.session.commit()
+    return num_project_collaborators_deleted
+
+def delete_all_collaborators():
+    num_collaborators_deleted = db.session.query(Collaborator).delete()
+    db.session.commit()
+    return num_collaborators_deleted
+
+### Message ###
+def add_message(message):
+    db.session.add(message)
+    db.session.commit()
+
+def get_message(id):
+    message = Message.query.get(id)
+    return message
+
+def get_messages():
+    all_messages = Message.query.all()
+    result = messages_schema.dump(all_messages)
+    return result.data
+
+def get_project_messages(project_id):
+    project_messages = Message.query.filter_by(projectId=project_id).all()
+    result = messages_schema.dump(project_messages)
+    return result.data
+
+def update_message(updated_message):
+    message = Message.query.get(updated_message.id)
+    message.projectId = updated_message.projectId
+    message.autorId = updated_message.autorId
+    message.time = updated_message.time
+    db.session.commit()
+    return message
+
+def delete_project_messages(project_id):
+    num_project_messages_deleted = Message.query.filter_by(projectId=project_id).delete()
+    db.session.commit()
+    return num_project_messages_deleted
+
+def delete_all_messages():
+    num_messages_deleted = db.session.query(Message).delete()
+    db.session.commit()
+    return num_messages_deleted
\ No newline at end of file
diff --git a/backend/2/socketio_helper.py b/backend/2/socketio_helper.py
new file mode 100644
index 00000000..6dfe599e
--- /dev/null
+++ b/backend/2/socketio_helper.py
@@ -0,0 +1,301 @@
+import os, shutil
+
+from flask import json
+from flask_socketio import SocketIO, send, emit, join_room, leave_room
+import diff_match_patch as dmp_module
+from simpleflock import SimpleFlock
+
+from __init__ import app
+import database_helper as db
+
+SIMPLE_FLOCK_TIMEOUT = 10
+
+socketio = SocketIO(app)
+dmp = dmp_module.diff_match_patch()
+
+project_viewers = dict()
+
+def has_viewer(username, project_id):
+    for viewer in project_viewers[str(project_id)]:
+        if viewer['username'] == username:
+            return True
+    return False
+
+def set_viewer(viewer, project_id):
+    filtered_viewers = []
+    for curr_viewer in project_viewers[str(project_id)]:
+        if curr_viewer['username'] != viewer['username']:
+            filtered_viewers.append(curr_viewer)
+    project_viewers[str(project_id)].append(viewer)
+    project_viewers[str(project_id)] = filtered_viewers
+
+def add_project_viewer(project_id, username):
+    viewer = {
+        'username': username,
+        'cursor': {
+            'fileId': None,
+            'position': None,
+            'added': False
+        }
+    }
+    if not str(project_id) in project_viewers.keys():
+        project_viewers[str(project_id)] = [viewer]
+    elif not has_viewer(username, project_id):
+        project_viewers[str(project_id)].append(viewer)
+    else:
+        set_viewer(viewer, project_id)
+
+def remove_project_viewer(project_id, username):
+    if str(project_id) in project_viewers.keys() and \
+    has_viewer(username, project_id):
+        viewers = []
+        for viewer in project_viewers[str(project_id)]:
+            if viewer['username'] != username:
+                viewers.append(viewer)
+        project_viewers[str(project_id)] = viewers
+
+
+## Join user room
+@socketio.on('joinUserRoom')
+def on_join_user_room(data):
+    data = json.loads(data)
+    user_id = data['userId']
+    room = 'user' + str(user_id)
+    join_room(room)
+    emit('joinUserRoom', {
+        'on': 'joinUserRoom',
+        'userId': user_id
+        }, room=room)
+
+@socketio.on('leaveUserRoom')
+def on_leave_user_room(data):
+    data = json.loads(data)
+    user_id = data['userId']
+    room = 'user' + str(user_id)
+    leave_room(room)
+    emit('leaveUserRoom', {
+        'on': 'leaveUserRoom',
+        'userId': user_id
+        }, room=room)
+
+## Join project room
+@socketio.on('joinProjectRoom')
+def on_join_project_room(data):
+    data = json.loads(data)
+    username = data['username']
+    project_id = data['projectId']
+    room = 'project' + str(project_id)
+    join_room(room)
+    emit('joinProjectRoom', {
+        'on': 'joinProjectRoom',
+        'username': username,
+        'projectId' : project_id
+        }, room=room)
+
+@socketio.on('leaveProjectRoom')
+def on_leave_project_room(data):
+    data = json.loads(data)
+    username = data['username']
+    project_id = data['projectId']
+    room = 'project' + str(project_id)
+    leave_room(room)
+    emit('leaveProjectRoom', {
+        'on': 'leaveProjectRoom',
+        'username': username,
+        'projectId' : project_id
+        }, room=room)
+
+## Project handling ##
+@socketio.on('projectCreated')
+def handle_project_created(data):
+    project = json.loads(data)
+    emit('projectCreated', {
+        'on': 'projectCreated',
+        'project': project
+        }, room = 'project' + str(project['id']))
+
+@socketio.on('projectChanged')
+def handle_project_changed(data):
+    project = json.loads(data)
+    emit('projectChanged', {
+        'on': 'projectChanged',
+        'project': project
+        }, room = 'project' + str(project['id']))
+
+@socketio.on('projectDeleted')
+def handle_project_deleted(data):
+    projectId = json.loads(data)
+    emit('projectDeleted', {
+        'on': 'projectDeleted',
+        'projectId': projectId
+        }, room = 'project' + str(projectId))
+
+@socketio.on('collaboratorAdded')
+def handle_collaborator_added(data):
+    collaborator = json.loads(data)
+    emit('collaboratorAdded', {
+        'on': 'collaboratorAdded',
+        'collaborator': collaborator
+        }, room = 'user' + str(collaborator['userId']))
+
+@socketio.on('collaboratorChanged')
+def handle_collaborator_changed(data):
+    collaborator = json.loads(data)
+    emit('collaboratorChanged', {
+        'on': 'collaboratorChanged',
+        'collaborator': collaborator
+        }, room = 'project' + str(collaborator['projectId']))
+
+## Join open project room
+@socketio.on('joinOpenProjectRoom')
+def on_join_open_project_room(data):
+    data = json.loads(data)
+    username = data['username']
+    project_id = data['projectId']
+    room = 'openProject' + str(project_id)
+    join_room(room)
+    add_project_viewer(project_id, username)
+
+    if not os.path.exists(str(project_id)):
+        os.mkdir(str(project_id))
+        for file in db.get_project_files(project_id):
+            dir_name = str(project_id) + ('/' + file['parent'] if file['parent'] else '')
+            if not os.path.exists(dir_name):
+                os.makedirs(dir_name)
+            if not file['isFolder']:
+                f = open(dir_name + '/' + file['name'], "a+")
+                f.write(file['content'])
+                f.close()
+
+    emit('joinOpenProjectRoom', {
+        'on': 'joinOpenProjectRoom',
+        'username': username,
+        'viewers': project_viewers[str(project_id)]
+        }, room=room)
+
+@socketio.on('leaveOpenProjectRoom')
+def on_leave_open_project_room(data):
+    data = json.loads(data)
+    username = data['username']
+    project_id = data['projectId']
+    room = 'openProject' + str(project_id)
+    leave_room(room)
+    remove_project_viewer(project_id, username)
+
+    if not project_viewers[str(project_id)] and os.path.exists(str(project_id)):
+        shutil.rmtree(str(project_id))
+
+    emit('leaveOpenProjectRoom', {
+        'on': 'leaveOpenProjectRoom',
+        'username': username,
+        'viewers': project_viewers[str(project_id)]
+        }, room=room)
+
+## File handling ##
+@socketio.on('fileCreated')
+def handle_file_created(data):
+    file = json.loads(data)
+    emit('fileCreated', {
+        'on': 'fileCreated',
+        'file': file
+        }, room = 'openProject' + str(file['projectId']))
+
+@socketio.on('fileChanged')
+def handle_file_changed(data):
+    file = json.loads(data)
+    emit('fileChanged', {
+        'on': 'fileChanged',
+        'file': file
+        }, room = 'openProject' + str(file['projectId']))
+
+
+@socketio.on('fileContentChanged')
+def handle_file_content_changed(data):
+    data = json.loads(data)
+    project_id = data['projectId']
+    file_id = data['fileId']
+    patches_text = data['patchesText']
+    patches = dmp.patch_fromText(patches_text)
+
+    file = db.get_file(file_id)
+    file.content = dmp.patch_apply(patches, file.content)[0]
+    db.update_file(file)
+
+    # Update file locally on server if project is open
+    open_project_path = str(file.projectId) + '/'
+    if os.path.exists(open_project_path):
+        if file.parent:
+            file_path = open_project_path + file.parent + '/' + file.name
+        else:
+            file_path = open_project_path + file.name
+        if os.path.exists(file_path):
+            with SimpleFlock(str(file.projectId) + 'lock', SIMPLE_FLOCK_TIMEOUT):
+                if os.path.exists(file_path):
+                    f = open(file_path, 'w')
+                    f.write(file.content)
+                    f.close()
+
+    emit('fileContentChanged', {
+        'on': 'fileContentChanged',
+        'projectId': project_id,
+        'fileId': file_id,
+        'patchesText' : patches_text
+        }, room = 'openProject' + str(project_id), include_self=False)
+
+@socketio.on('fileDeleted')
+def handle_file_deleted(data):
+    file = json.loads(data)
+    emit('fileDeleted', {
+        'on': 'fileDeleted',
+        'file': file
+        }, room = 'openProject' + str(file['projectId']))
+
+## Chat ##
+@socketio.on('message')
+def handle_message(data):
+    data = json.loads(data)
+    
+    projectId = data['projectId']
+    username = data['username']
+    message = data['message']
+
+    author = db.get_user_from_username(username)
+    new_message = db.Message(
+        projectId,
+        author.id,
+        message
+    )
+    db.add_message(new_message)
+    
+    emit('message', {
+        'on': 'message',
+        'author': db.user_schema.dumps(author),
+        'time': new_message.time,
+        'message': message
+        }, room = 'openProject' + str(projectId))
+
+## Editor ##
+@socketio.on('viewerCursorChanged')
+def handle_viewer_cursor_changed(data):
+    data = json.loads(data)
+    project_id = data['projectId']
+    file_id = data['fileId']
+    username = data['username']
+    cursor_position = data['cursorPosition']
+
+    if not str(project_id) in project_viewers.keys() \
+            or not has_viewer(username, project_id):
+        add_project_viewer(project_id, username)
+
+    for viewer in project_viewers[str(project_id)]:
+        if viewer['username'] == username:
+            viewer['cursor'] = {
+                'fileId': file_id,
+                'position': cursor_position
+        }
+    emit('viewerCursorChanged', {
+        'on': 'viewerCursorChanged',
+        'viewer': username,
+        'fileId': file_id,
+        'cursorPosition': cursor_position
+        }, room = 'openProject' + str(project_id), include_self=False)
\ No newline at end of file
diff --git a/backend/2/test.py b/backend/2/test.py
new file mode 100644
index 00000000..735bdcc8
--- /dev/null
+++ b/backend/2/test.py
@@ -0,0 +1 @@
+print('Hello world!')
\ No newline at end of file
diff --git a/backend/Pipfile b/backend/Pipfile
index 2132e152..8f2ac692 100644
--- a/backend/Pipfile
+++ b/backend/Pipfile
@@ -17,6 +17,7 @@ pyjwt = "*"
 bson = "*"
 flask-socketio = "*"
 diff-match-patch = "*"
+simpleflock = "*"
 
 [requires]
 python_version = "3.6"
diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock
index 4aca4b48..dc7c95c1 100644
--- a/backend/Pipfile.lock
+++ b/backend/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "c40b8fdfa182b209d5d3d2e5c2cac67b40cbc77ff95f0ae845d722a57fae950b"
+            "sha256": "0cbc6707a54e77c6ff91fec870c927dfba819c9aaa97d84191e5ebef71e9cfba"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -96,11 +96,11 @@
         },
         "flask": {
             "hashes": [
-                "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
-                "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
+                "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3",
+                "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"
             ],
             "index": "pypi",
-            "version": "==1.0.2"
+            "version": "==1.0.3"
         },
         "flask-bcrypt": {
             "hashes": [
@@ -134,11 +134,11 @@
         },
         "flask-socketio": {
             "hashes": [
-                "sha256:8d8f9f104db5ddff1b06ba322d8e158881d590144199c993fe26cf53218c7edd",
-                "sha256:f9b9c95c82b62381862fd3bc55cea7fcc08e787f6bb63fdc79a2f258df2bdc9a"
+                "sha256:841dc9636fcf0659223745434b7ceef56f716fede2311ec26cb97af30648ca22",
+                "sha256:f44b2fc91149c1b2b522c8fd64824d10d4e38d535c2cb76623c22b1e9eeb9de9"
             ],
             "index": "pypi",
-            "version": "==3.3.2"
+            "version": "==4.0.0"
         },
         "flask-sqlalchemy": {
             "hashes": [
@@ -233,17 +233,24 @@
         },
         "python-engineio": {
             "hashes": [
-                "sha256:a89d2cff7f9b447d37c450c2666101d97043b4b22524bf4efaafcb9784c9f7f4",
-                "sha256:fd14357f0b854de729cc2dba960ceba1d20da973c91f289110807f0bb2f69935"
+                "sha256:06bd817c0ea50df62e0ea31b89f427ba4ef6f954e09cb0f9036bcfce8193e304",
+                "sha256:0886831a4ce9f0e8293b927c4b8e4a3dc6dba4a55830abb9c170b12b34d453e9"
             ],
-            "version": "==3.5.1"
+            "version": "==3.6.0"
         },
         "python-socketio": {
             "hashes": [
-                "sha256:aa702157694d55a743fb6f1cc0bd1af58fbfda8a7d71d747d4b12d6dac29cab3",
-                "sha256:cd225eb0bb3b348665727cfaafad1e455ee13b8fd9ea9ff2691082b88b9a9444"
+                "sha256:80810f40a5b276937a95d4944229fa9c20ee61963365387549fd943589661a6f",
+                "sha256:c128d320b23ce3c4323ca5b0e20f602a075ceea56f368f6b9f52bed48d97df71"
             ],
-            "version": "==3.1.2"
+            "version": "==4.0.3"
+        },
+        "simpleflock": {
+            "hashes": [
+                "sha256:0dfebcbe4ae574fdd01466be002dc8cd274ba350b90d11ef910c4bc9766c59eb"
+            ],
+            "index": "pypi",
+            "version": "==0.0.3"
         },
         "six": {
             "hashes": [
diff --git a/backend/app.py b/backend/app.py
index d631cf6b..d2bda151 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -2,17 +2,20 @@ import os, sys, subprocess, shutil
 import random
 
 import logging
+from subprocess import Popen, PIPE
+from datetime import datetime
 from flask import Flask, request, jsonify, Response, json
 from flask_jwt_extended import JWTManager
 from flask_jwt_extended import (create_access_token, create_refresh_token,
                                 jwt_required, jwt_refresh_token_required, get_jwt_identity)
 from flask_bcrypt import Bcrypt
+from flask_socketio import emit
 from base64 import b64decode
-from datetime import datetime
+from simpleflock import SimpleFlock
 
 from __init__ import app
 import database_helper as db
-from socketio_helper import socketio
+from socketio_helper import socketio, SIMPLE_FLOCK_TIMEOUT
 
 flask_bcrypt = Bcrypt(app)
 jwt = JWTManager(app)
@@ -30,7 +33,8 @@ def generate_response(response, status, headers={}):
     response.status_code = status
     response.headers = {**{
         'Content-Type': 'application/json',
-        'Access-Control-Expose-Headers' : 'X-Auth-Token',
+        'Access-Control-Expose-Headers': 'X-Auth-Token',
+        'Access-Control-Allow-Origin': '*'
         }, **headers}
     return response
 
@@ -265,7 +269,6 @@ def new_project():
     else:
         new_project = db.Project(title, creatorId)
         db.add_project(new_project)
-        app.logger.error(new_project.id)
 
         if collaborators:
             for collaborator in collaborators:
@@ -389,27 +392,27 @@ def delete_project_request(project_id):
 @jwt_required
 def parse_file_request(project_id):
     if db.get_project(project_id):
-        os.mkdir(str(project_id))
-        for file in db.get_project_files(project_id):
-            dir_name = str(project_id) + ('/' + file['parent'] if file['parent'] else '')
-            if not os.path.exists(dir_name):
-                os.makedirs(dir_name)
-            if not file['isFolder']:
-                f = open(dir_name + '/' + file['name'], "a+")
-                f.write(file['content'])
-                f.close()
-        result = subprocess.run(['pylint', '--output-format=json', str(project_id) + '/'],\
-            stdout=subprocess.PIPE).stdout.decode('utf-8')
-        shutil.rmtree(str(project_id))
+        if os.path.exists(str(project_id)):
+            with SimpleFlock(str(project_id) + 'lock', SIMPLE_FLOCK_TIMEOUT):
+                find_cmd = 'find ' + str(project_id) + '/ -type f -name "*.py"'
+                find = Popen(find_cmd, shell=True, stdout=PIPE)
+                files = find.communicate()[0].decode('utf-8')
+                files = ' '.join(files.splitlines())
+
+                pylint_cmd = 'pylint --output-format=json ' + files
+                pylint = Popen(pylint_cmd, shell=True, stdout=PIPE)
+                result = pylint.communicate()[0].decode('utf-8')
+        else:
+            result = '[]'
         return generate_response(jsonify({
             'success': True,
-            'message': 'Successfully parsed file.',
+            'message': 'Successfully parsed files.',
             'result': result
         }), OK_STATUS_CODE)
     else:
         return generate_response(jsonify({
             'success': False,
-            'message': 'No such file'
+            'message': 'No such project'
         }), BAD_REQUEST_STATUS_CODE)
 
 ### FILE ###
@@ -431,14 +434,30 @@ def create_file_request():
     project.edited = datetime.now()
     db.update_project(project)
 
+    # Add file locally on server if project is open
+    open_project_path = str(new_file.projectId) + '/'
+    if os.path.exists(open_project_path):
+        if new_file.parent:
+            file_path = open_project_path + new_file.parent + '/' + new_file.name
+        else:
+            file_path = open_project_path + new_file.name
+
+        if new_file.isFolder:
+            os.makedirs(file_path)
+        else:
+            f = open(file_path, 'w+')
+            f.write(new_file.content)
+            f.close()
+
     return generate_response(db.file_schema.jsonify(
         new_file), OK_STATUS_CODE)
 
 @app.route('/api/update_file/<file_id>', methods=['PUT'])
 @jwt_required
 def update_file_request(file_id):
-    # Update file
     file = request.json['file']
+    old_file = db.get_file(file['id'])
+
     updated_file = db.File(
         file['projectId'],
         file['name'],
@@ -452,6 +471,21 @@ def update_file_request(file_id):
     project.edited = datetime.now()
     db.update_project(project)
 
+    # Update file locally on server if project is open
+    open_project_path = str(project.id) + '/'
+    if old_file.parent:
+        old_path = open_project_path + old_file.parent + '/' + old_file.name
+    else:
+        old_path = open_project_path + old_file.name
+    if os.path.exists(old_path):
+        with SimpleFlock(str(project.id) + 'lock', SIMPLE_FLOCK_TIMEOUT):
+            if os.path.exists(old_path):
+                if file['parent']:
+                    new_path = open_project_path + file['parent'] + '/' + file['name']
+                else:
+                    new_path = open_project_path + file['name']
+                os.rename(old_path, new_path)
+
     return generate_response(db.file_schema.jsonify(
         db.update_file(updated_file)), OK_STATUS_CODE)
 
@@ -464,6 +498,20 @@ def delete_file_request(file_id):
     project.edited = datetime.now()
     db.update_project(project)
 
+    # Remove file locally on server if project is open
+    open_project_path = str(file.projectId) + '/'
+    if file.parent:
+        path = open_project_path + file.parent + '/' + file.name
+    else:
+        path = open_project_path + file.name
+    if os.path.exists(path):
+        with SimpleFlock(str(file.projectId) + 'lock', SIMPLE_FLOCK_TIMEOUT):
+            if os.path.exists(path):
+                if file.isFolder:
+                    shutil.rmtree(path)
+                else:
+                    os.remove(path)
+
     return generate_response(db.file_schema.jsonify(
         db.delete_file(file_id)), OK_STATUS_CODE)
 
diff --git a/backend/db.sqlite b/backend/db.sqlite
index 487f58053c2e48871332198feaef47f8ecd16bd9..8877bab2689f0f7d27a34cbe7a6def330c44a19f 100644
GIT binary patch
literal 282624
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

delta 2783
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
KB3>_3N+JN9llEKy

diff --git a/backend/socketio_helper.py b/backend/socketio_helper.py
index 1b2b7245..6dfe599e 100644
--- a/backend/socketio_helper.py
+++ b/backend/socketio_helper.py
@@ -1,9 +1,14 @@
-from __init__ import app
-import database_helper as db
+import os, shutil
 
 from flask import json
 from flask_socketio import SocketIO, send, emit, join_room, leave_room
 import diff_match_patch as dmp_module
+from simpleflock import SimpleFlock
+
+from __init__ import app
+import database_helper as db
+
+SIMPLE_FLOCK_TIMEOUT = 10
 
 socketio = SocketIO(app)
 dmp = dmp_module.diff_match_patch()
@@ -150,6 +155,18 @@ def on_join_open_project_room(data):
     room = 'openProject' + str(project_id)
     join_room(room)
     add_project_viewer(project_id, username)
+
+    if not os.path.exists(str(project_id)):
+        os.mkdir(str(project_id))
+        for file in db.get_project_files(project_id):
+            dir_name = str(project_id) + ('/' + file['parent'] if file['parent'] else '')
+            if not os.path.exists(dir_name):
+                os.makedirs(dir_name)
+            if not file['isFolder']:
+                f = open(dir_name + '/' + file['name'], "a+")
+                f.write(file['content'])
+                f.close()
+
     emit('joinOpenProjectRoom', {
         'on': 'joinOpenProjectRoom',
         'username': username,
@@ -164,6 +181,10 @@ def on_leave_open_project_room(data):
     room = 'openProject' + str(project_id)
     leave_room(room)
     remove_project_viewer(project_id, username)
+
+    if not project_viewers[str(project_id)] and os.path.exists(str(project_id)):
+        shutil.rmtree(str(project_id))
+
     emit('leaveOpenProjectRoom', {
         'on': 'leaveOpenProjectRoom',
         'username': username,
@@ -200,6 +221,20 @@ def handle_file_content_changed(data):
     file.content = dmp.patch_apply(patches, file.content)[0]
     db.update_file(file)
 
+    # Update file locally on server if project is open
+    open_project_path = str(file.projectId) + '/'
+    if os.path.exists(open_project_path):
+        if file.parent:
+            file_path = open_project_path + file.parent + '/' + file.name
+        else:
+            file_path = open_project_path + file.name
+        if os.path.exists(file_path):
+            with SimpleFlock(str(file.projectId) + 'lock', SIMPLE_FLOCK_TIMEOUT):
+                if os.path.exists(file_path):
+                    f = open(file_path, 'w')
+                    f.write(file.content)
+                    f.close()
+
     emit('fileContentChanged', {
         'on': 'fileContentChanged',
         'projectId': project_id,
-- 
GitLab