diff --git a/backend/19/Folder2/nested.py b/backend/19/Folder2/nested.py new file mode 100644 index 0000000000000000000000000000000000000000..7ddfc8f8590cc02ffa25d601ed760a7429b5d482 --- /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 0000000000000000000000000000000000000000..f579e6f8fcda7ae8245520403ea9c07877544954 --- /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 0000000000000000000000000000000000000000..023d63f9ecccb018140d50b607daaa2633193387 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/2/Folder/app.py b/backend/2/Folder/app.py new file mode 100644 index 0000000000000000000000000000000000000000..f9204ee55f1fd86c5e5f407cdcd1ec3d0f072d66 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/2/Folder/hello.py b/backend/2/Folder/hello.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/2/app.py b/backend/2/app.py new file mode 100644 index 0000000000000000000000000000000000000000..63fe476518fedf45c96e340c20b0c566e0c7e4e0 --- /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 0000000000000000000000000000000000000000..8bfe3a7cc18136540240475bf855010289d980b5 --- /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 0000000000000000000000000000000000000000..6dfe599e7c1c9a707e16d90db91418c483f5c8d9 --- /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 0000000000000000000000000000000000000000..735bdcc87b647754c06a0e86535f7bc47694dd8e --- /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 2132e1525c3bc907ee7495a61c14ba536b7d95b0..8f2ac692ecc9fb06a457e1eaf75178fa336109fa 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 4aca4b4834766ab23c9a2505803556b3a1b315a9..dc7c95c197e6a58dc5c0167d5cfc25d470d2149d 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 d631cf6b23f3eb50d88093ffb20d081f10ac9092..d2bda1516ceb7243ac270dcbdc48e501d694f367 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 Binary files a/backend/db.sqlite and b/backend/db.sqlite differ diff --git a/backend/socketio_helper.py b/backend/socketio_helper.py index 1b2b7245047dd11954a7136e9f554114fc82a6df..6dfe599e7c1c9a707e16d90db91418c483f5c8d9 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,