A GUI web-based tool to manage gem5 Resources. Can manage in two data sources, a MongoDB database or a JSON file. The JSON file can be both local or remote. JSON files are written to a temporary file before writing to the local file. The Manager supports the following functions on a high-level: - searching for a resource by ID - navigating to a resource version - adding a new resource - adding a new version to a resource - editing any information within a searched resource (while enforcing the gem5 Resources schema found at: https://resources.gem5.org/gem5-resources-schema.json) - deleting a resource version - undo and redo up to the last 10 operations The Manager also allows a user to save a session through localStorage and re-access it through a password securely. This patch also provides a Command Line Interface tool mainly for MongoDB-related functions. This CLI tool can currently: - backup a MongoDB collection to a JSON file - restore a JSON file to a MongoDB collection - search for a resource through its ID and view its JSON object - make a JSON file that is compliant with the gem5 Resources Schema Co-authored-by: Parth Shah <helloparthshah@gmail.com> Co-authored-by: Harshil2107 <harshilp2107@gmail.com> Co-authored-by: aarsli <arsli@ucdavis.edu> Change-Id: I8107f609c869300b5323d4942971a7ce7c28d6b5 Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/71218 Reviewed-by: Bobby Bruce <bbruce@ucdavis.edu> Tested-by: kokoro <noreply+kokoro@google.com> Maintainer: Bobby Bruce <bbruce@ucdavis.edu>
885 lines
32 KiB
Python
885 lines
32 KiB
Python
# Copyright (c) 2023 The Regents of the University of California
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are
|
|
# met: redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer;
|
|
# redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution;
|
|
# neither the name of the copyright holders nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
from flask import (
|
|
render_template,
|
|
Flask,
|
|
request,
|
|
redirect,
|
|
url_for,
|
|
make_response,
|
|
)
|
|
from bson import json_util
|
|
import json
|
|
import jsonschema
|
|
import requests
|
|
import markdown
|
|
import base64
|
|
import secrets
|
|
from pathlib import Path
|
|
from werkzeug.utils import secure_filename
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|
from cryptography.exceptions import InvalidSignature
|
|
from api.json_client import JSONClient
|
|
from api.mongo_client import MongoDBClient
|
|
|
|
databases = {}
|
|
|
|
response = requests.get(
|
|
"https://resources.gem5.org/gem5-resources-schema.json"
|
|
)
|
|
schema = json.loads(response.content)
|
|
|
|
|
|
UPLOAD_FOLDER = Path("database/")
|
|
TEMP_UPLOAD_FOLDER = Path("database/.tmp/")
|
|
CONFIG_FILE = Path("instance/config.py")
|
|
SESSIONS_COOKIE_KEY = "sessions"
|
|
ALLOWED_EXTENSIONS = {"json"}
|
|
CLIENT_TYPES = ["mongodb", "json"]
|
|
|
|
|
|
app = Flask(__name__, instance_relative_config=True)
|
|
|
|
|
|
if not CONFIG_FILE.exists():
|
|
CONFIG_FILE.parent.mkdir()
|
|
with CONFIG_FILE.open("w+") as f:
|
|
f.write(f"SECRET_KEY = {secrets.token_bytes(32)}")
|
|
|
|
|
|
app.config.from_pyfile(CONFIG_FILE.name)
|
|
|
|
|
|
# Sorts keys in any serialized dict
|
|
# Default = True
|
|
# Set False to persevere JSON key order
|
|
app.json.sort_keys = False
|
|
|
|
|
|
def startup_config_validation():
|
|
"""
|
|
Validates the startup configuration.
|
|
|
|
Raises:
|
|
ValueError: If the 'SECRET_KEY' is not set or is not of type 'bytes'.
|
|
"""
|
|
if not app.secret_key:
|
|
raise ValueError("SECRET_KEY not set")
|
|
if not isinstance(app.secret_key, bytes):
|
|
raise ValueError("SECRET_KEY must be of type 'bytes'")
|
|
|
|
|
|
def startup_dir_file_validation():
|
|
"""
|
|
Validates the startup directory and file configuration.
|
|
|
|
Creates the required directories if they do not exist.
|
|
"""
|
|
for dir in [UPLOAD_FOLDER, TEMP_UPLOAD_FOLDER]:
|
|
if not dir.is_dir():
|
|
dir.mkdir()
|
|
|
|
|
|
with app.app_context():
|
|
startup_config_validation()
|
|
startup_dir_file_validation()
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
"""
|
|
Renders the index HTML template.
|
|
|
|
:return: The rendered index HTML template.
|
|
"""
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route("/login/mongodb")
|
|
def login_mongodb():
|
|
"""
|
|
Renders the MongoDB login HTML template.
|
|
|
|
:return: The rendered MongoDB login HTML template.
|
|
"""
|
|
return render_template("login/login_mongodb.html")
|
|
|
|
|
|
@app.route("/login/json")
|
|
def login_json():
|
|
"""
|
|
Renders the JSON login HTML template.
|
|
|
|
:return: The rendered JSON login HTML template.
|
|
"""
|
|
return render_template("login/login_json.html")
|
|
|
|
|
|
@app.route("/validateMongoDB", methods=["POST"])
|
|
def validate_mongodb():
|
|
"""
|
|
Validates the MongoDB connection parameters and redirects to the editor route if successful.
|
|
|
|
This route expects a POST request with a JSON payload containing an alias for the session and the listed parameters in order to validate the MongoDB instance.
|
|
|
|
This route expects the following JSON payload parameters:
|
|
- uri: The MongoDB connection URI.
|
|
- collection: The name of the collection in the MongoDB database.
|
|
- database: The name of the MongoDB database.
|
|
- alias: The value by which the session will be keyed in `databases`.
|
|
|
|
If the 'uri' parameter is empty, a JSON response with an error message and status code 400 (Bad Request) is returned.
|
|
If the connection parameters are valid, the route redirects to the 'editor' route with the appropriate query parameters.
|
|
|
|
:return: A redirect response to the 'editor' route or a JSON response with an error message and status code 400.
|
|
"""
|
|
global databases
|
|
try:
|
|
databases[request.json["alias"]] = MongoDBClient(
|
|
mongo_uri=request.json["uri"],
|
|
database_name=request.json["database"],
|
|
collection_name=request.json["collection"],
|
|
)
|
|
except Exception as e:
|
|
return {"error": str(e)}, 400
|
|
return redirect(
|
|
url_for("editor", alias=request.json["alias"]),
|
|
302,
|
|
)
|
|
|
|
|
|
@app.route("/validateJSON", methods=["GET"])
|
|
def validate_json_get():
|
|
"""
|
|
Validates the provided JSON URL and redirects to the editor route if successful.
|
|
|
|
This route expects the following query parameters:
|
|
- q: The URL of the JSON file.
|
|
- filename: An optional filename for the uploaded JSON file.
|
|
|
|
If the 'q' parameter is empty, a JSON response with an error message and status code 400 (Bad Request) is returned.
|
|
If the JSON URL is valid, the function retrieves the JSON content, saves it to a file, and redirects to the 'editor'
|
|
route with the appropriate query parameters.
|
|
|
|
:return: A redirect response to the 'editor' route or a JSON response with an error message and status code 400.
|
|
"""
|
|
filename = request.args.get("filename")
|
|
url = request.args.get("q")
|
|
if not url:
|
|
return {"error": "empty"}, 400
|
|
response = requests.get(url)
|
|
if response.status_code != 200:
|
|
return {"error": "invalid status"}, response.status_code
|
|
filename = secure_filename(request.args.get("filename"))
|
|
path = UPLOAD_FOLDER / filename
|
|
if (UPLOAD_FOLDER / filename).is_file():
|
|
temp_path = TEMP_UPLOAD_FOLDER / filename
|
|
with temp_path.open("wb") as f:
|
|
f.write(response.content)
|
|
return {"conflict": "existing file in server"}, 409
|
|
with path.open("wb") as f:
|
|
f.write(response.content)
|
|
global databases
|
|
if filename in databases:
|
|
return {"error": "alias already exists"}, 409
|
|
try:
|
|
databases[filename] = JSONClient(filename)
|
|
except Exception as e:
|
|
return {"error": str(e)}, 400
|
|
return redirect(
|
|
url_for("editor", alias=filename),
|
|
302,
|
|
)
|
|
|
|
|
|
@app.route("/validateJSON", methods=["POST"])
|
|
def validate_json_post():
|
|
"""
|
|
Validates and processes the uploaded JSON file.
|
|
|
|
This route expects a file with the key 'file' in the request files.
|
|
If the file is not present, a JSON response with an error message
|
|
and status code 400 (Bad Request) is returned.
|
|
If the file already exists in the server, a JSON response with a
|
|
conflict error message and status code 409 (Conflict) is returned.
|
|
If the file's filename conflicts with an existing alias, a JSON
|
|
response with an error message and status code 409 (Conflict) is returned.
|
|
If there is an error while processing the JSON file, a JSON response
|
|
with the error message and status code 400 (Bad Request) is returned.
|
|
If the file is successfully processed, a redirect response to the
|
|
'editor' route with the appropriate query parameters is returned.
|
|
|
|
:return: A JSON response with an error message and
|
|
status code 400 or 409, or a redirect response to the 'editor' route.
|
|
"""
|
|
temp_path = None
|
|
if "file" not in request.files:
|
|
return {"error": "empty"}, 400
|
|
file = request.files["file"]
|
|
filename = secure_filename(file.filename)
|
|
path = UPLOAD_FOLDER / filename
|
|
if path.is_file():
|
|
temp_path = TEMP_UPLOAD_FOLDER / filename
|
|
file.save(temp_path)
|
|
return {"conflict": "existing file in server"}, 409
|
|
file.save(path)
|
|
global databases
|
|
if filename in databases:
|
|
return {"error": "alias already exists"}, 409
|
|
try:
|
|
databases[filename] = JSONClient(filename)
|
|
except Exception as e:
|
|
return {"error": str(e)}, 400
|
|
return redirect(
|
|
url_for("editor", alias=filename),
|
|
302,
|
|
)
|
|
|
|
|
|
@app.route("/existingJSON", methods=["GET"])
|
|
def existing_json():
|
|
"""
|
|
Handles the request for an existing JSON file.
|
|
|
|
This route expects a query parameter 'filename'
|
|
specifying the name of the JSON file.
|
|
If the file is not present in the 'databases',
|
|
it tries to create a 'JSONClient' instance for the file.
|
|
If there is an error while creating the 'JSONClient'
|
|
instance, a JSON response with the error message
|
|
and status code 400 (Bad Request) is returned.
|
|
If the file is present in the 'databases', a redirect
|
|
response to the 'editor' route with the appropriate
|
|
query parameters is returned.
|
|
|
|
:return: A JSON response with an error message
|
|
and status code 400, or a redirect response to the 'editor' route.
|
|
"""
|
|
filename = request.args.get("filename")
|
|
global databases
|
|
if filename not in databases:
|
|
try:
|
|
databases[filename] = JSONClient(filename)
|
|
except Exception as e:
|
|
return {"error": str(e)}, 400
|
|
return redirect(
|
|
url_for("editor", alias=filename),
|
|
302,
|
|
)
|
|
|
|
|
|
@app.route("/existingFiles", methods=["GET"])
|
|
def get_existing_files():
|
|
"""
|
|
Retrieves the list of existing files in the upload folder.
|
|
|
|
This route returns a JSON response containing the names of the existing files in the upload folder configured in the
|
|
Flask application.
|
|
|
|
:return: A JSON response with the list of existing files.
|
|
"""
|
|
files = [f.name for f in UPLOAD_FOLDER.iterdir() if f.is_file()]
|
|
return json.dumps(files)
|
|
|
|
|
|
@app.route("/resolveConflict", methods=["GET"])
|
|
def resolve_conflict():
|
|
"""
|
|
Resolves file conflict with JSON files.
|
|
|
|
This route expects the following query parameters:
|
|
- filename: The name of the file that is conflicting or an updated name for it to resolve the name conflict
|
|
- resolution: A resolution option, defined as follows:
|
|
- clearInput: Deletes the conflicting file and does not proceed with login
|
|
- openExisting: Opens the existing file in `UPLOAD_FOLDER`
|
|
- overwrite: Overwrites the existing file with the conflicting file
|
|
- newFilename: Renames conflicting file, moving it to `UPLOAD_FOLDER`
|
|
|
|
If the resolution parameter is not from the list given, an error is returned.
|
|
|
|
The conflicting file in `TEMP_UPLOAD_FOLDER` is deleted.
|
|
|
|
:return: A JSON response containing an error, or a success response, or a redirect to the editor.
|
|
"""
|
|
filename = secure_filename(request.args.get("filename"))
|
|
resolution = request.args.get("resolution")
|
|
resolution_options = [
|
|
"clearInput",
|
|
"openExisting",
|
|
"overwrite",
|
|
"newFilename",
|
|
]
|
|
temp_path = TEMP_UPLOAD_FOLDER / filename
|
|
if not resolution:
|
|
return {"error": "empty"}, 400
|
|
if resolution not in resolution_options:
|
|
return {"error": "invalid resolution"}, 400
|
|
if resolution == resolution_options[0]:
|
|
temp_path.unlink()
|
|
return {"success": "input cleared"}, 204
|
|
if resolution in resolution_options[-2:]:
|
|
next(TEMP_UPLOAD_FOLDER.glob("*")).replace(UPLOAD_FOLDER / filename)
|
|
if temp_path.is_file():
|
|
temp_path.unlink()
|
|
global databases
|
|
if filename in databases:
|
|
return {"error": "alias already exists"}, 409
|
|
try:
|
|
databases[filename] = JSONClient(filename)
|
|
except Exception as e:
|
|
return {"error": str(e)}, 400
|
|
return redirect(
|
|
url_for("editor", alias=filename),
|
|
302,
|
|
)
|
|
|
|
|
|
@app.route("/editor")
|
|
def editor():
|
|
"""
|
|
Renders the editor page based on the specified database type.
|
|
|
|
This route expects a GET request with specific query parameters:
|
|
- "alias": An optional alias for the MongoDB database.
|
|
|
|
The function checks if the query parameters are present. If not, it returns a 404 error.
|
|
|
|
The function determines the database type based on the instance of the client object stored in the databases['alias']. If the type is not in the
|
|
"CLIENT_TYPES" configuration, it returns a 404 error.
|
|
|
|
:return: The rendered editor template based on the specified database type.
|
|
"""
|
|
global databases
|
|
if not request.args:
|
|
return render_template("404.html"), 404
|
|
alias = request.args.get("alias")
|
|
if alias not in databases:
|
|
return render_template("404.html"), 404
|
|
|
|
client_type = ""
|
|
if isinstance(databases[alias], JSONClient):
|
|
client_type = CLIENT_TYPES[1]
|
|
elif isinstance(databases[alias], MongoDBClient):
|
|
client_type = CLIENT_TYPES[0]
|
|
else:
|
|
return render_template("404.html"), 404
|
|
|
|
response = make_response(
|
|
render_template("editor.html", client_type=client_type, alias=alias)
|
|
)
|
|
|
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
response.headers["Pragma"] = "no-cache"
|
|
response.headers["Expires"] = "0"
|
|
|
|
return response
|
|
|
|
|
|
@app.route("/help")
|
|
def help():
|
|
"""
|
|
Renders the help page.
|
|
|
|
This route reads the contents of the "help.md" file located in the "static" folder and renders it as HTML using the
|
|
Markdown syntax. The rendered HTML is then passed to the "help.html" template for displaying the help page.
|
|
|
|
:return: The rendered help page HTML.
|
|
"""
|
|
with Path("static/help.md").open("r") as f:
|
|
return render_template(
|
|
"help.html", rendered_html=markdown.markdown(f.read())
|
|
)
|
|
|
|
|
|
@app.route("/find", methods=["POST"])
|
|
def find():
|
|
"""
|
|
Finds a resource based on the provided search criteria.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session which is to be searched for the
|
|
resource and the search criteria.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is returned.
|
|
|
|
The Client API is used to find the resource by calling `find_resource()` on the session where the operation is
|
|
accomplished by the concrete client class.
|
|
|
|
The result of the `find_resource` operation is returned as a JSON response.
|
|
|
|
:return: A JSON response containing the result of the `find_resource` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
return database.find_resource(request.json)
|
|
|
|
|
|
@app.route("/update", methods=["POST"])
|
|
def update():
|
|
"""
|
|
Updates a resource with provided changes.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session which contains the resource
|
|
that is to be updated and the data for updating the resource.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is returned.
|
|
|
|
The Client API is used to update the resource by calling `update_resource()` on the session where the operation is
|
|
accomplished by the concrete client class.
|
|
|
|
The `_add_to_stack` function of the session is called to insert the operation, update, and necessary data onto the revision
|
|
operations stack.
|
|
|
|
The result of the `update_resource` operation is returned as a JSON response. It contains the original and the modified resources.
|
|
|
|
:return: A JSON response containing the result of the `update_resource` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
original_resource = request.json["original_resource"]
|
|
modified_resource = request.json["resource"]
|
|
status = database.update_resource(
|
|
{
|
|
"original_resource": original_resource,
|
|
"resource": modified_resource,
|
|
}
|
|
)
|
|
database._add_to_stack(
|
|
{
|
|
"operation": "update",
|
|
"resource": {
|
|
"original_resource": modified_resource,
|
|
"resource": original_resource,
|
|
},
|
|
}
|
|
)
|
|
return status
|
|
|
|
|
|
@app.route("/versions", methods=["POST"])
|
|
def getVersions():
|
|
"""
|
|
Retrieves the versions of a resource based on the provided search criteria.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session which contains the resource
|
|
whose versions are to be retrieved and the search criteria.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is returned.
|
|
|
|
The Client API is used to get the versions of a resource by calling `get_versions()` on the session where the operation is
|
|
accomplished by the concrete client class.
|
|
|
|
The result of the `get_versions` operation is returned as a JSON response.
|
|
|
|
:return: A JSON response containing the result of the `get_versions` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
return database.get_versions(request.json)
|
|
|
|
|
|
@app.route("/categories", methods=["GET"])
|
|
def getCategories():
|
|
"""
|
|
Retrieves the categories of the resources.
|
|
|
|
This route returns a JSON response containing the categories of the resources. The categories are obtained from the
|
|
"enum" property of the "category" field in the schema.
|
|
|
|
:return: A JSON response with the categories of the resources.
|
|
"""
|
|
return json.dumps(schema["properties"]["category"]["enum"])
|
|
|
|
|
|
@app.route("/schema", methods=["GET"])
|
|
def getSchema():
|
|
"""
|
|
Retrieves the schema definition of the resources.
|
|
|
|
This route returns a JSON response containing the schema definition of the resources. The schema is obtained from the
|
|
`schema` variable.
|
|
|
|
:return: A JSON response with the schema definition of the resources.
|
|
"""
|
|
return json_util.dumps(schema)
|
|
|
|
|
|
@app.route("/keys", methods=["POST"])
|
|
def getFields():
|
|
"""
|
|
Retrieves the required fields for a specific category based on the provided data.
|
|
|
|
This route expects a POST request with a JSON payload containing the data for retrieving the required fields.
|
|
The function constructs an empty object `empty_object` with the "category" and "id" values from the request payload.
|
|
|
|
The function then uses the JSONSchema validator to validate the `empty_object` against the `schema`. It iterates
|
|
through the validation errors and handles two types of errors:
|
|
|
|
1. "is a required property" error: If a required property is missing in the `empty_object`, the function retrieves
|
|
the default value for that property from the schema and sets it in the `empty_object`.
|
|
|
|
2. "is not valid under any of the given schemas" error: If a property is not valid under the current schema, the
|
|
function evolves the validator to use the schema corresponding to the requested category. It then iterates
|
|
through the validation errors again and handles any missing required properties as described in the previous
|
|
step.
|
|
|
|
Finally, the `empty_object` with the required fields populated (including default values if applicable) is returned
|
|
as a JSON response.
|
|
|
|
:return: A JSON response containing the `empty_object` with the required fields for the specified category.
|
|
"""
|
|
empty_object = {
|
|
"category": request.json["category"],
|
|
"id": request.json["id"],
|
|
}
|
|
validator = jsonschema.Draft7Validator(schema)
|
|
errors = list(validator.iter_errors(empty_object))
|
|
for error in errors:
|
|
if "is a required property" in error.message:
|
|
required = error.message.split("'")[1]
|
|
empty_object[required] = error.schema["properties"][required][
|
|
"default"
|
|
]
|
|
if "is not valid under any of the given schemas" in error.message:
|
|
validator = validator.evolve(
|
|
schema=error.schema["definitions"][request.json["category"]]
|
|
)
|
|
for e in validator.iter_errors(empty_object):
|
|
if "is a required property" in e.message:
|
|
required = e.message.split("'")[1]
|
|
if "default" in e.schema["properties"][required]:
|
|
empty_object[required] = e.schema["properties"][
|
|
required
|
|
]["default"]
|
|
else:
|
|
empty_object[required] = ""
|
|
return json.dumps(empty_object)
|
|
|
|
|
|
@app.route("/delete", methods=["POST"])
|
|
def delete():
|
|
"""
|
|
Deletes a resource.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session from which a resource is to be
|
|
deleted and the data for deleting the resource.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is returned.
|
|
|
|
The Client API is used to delete the resource by calling `delete_resource()` on the session where the operation is
|
|
accomplished by the concrete client class.
|
|
|
|
The `_add_to_stack` function of the session is called to insert the operation, delete, and necessary data onto the revision
|
|
operations stack.
|
|
|
|
The result of the `delete` operation is returned as a JSON response.
|
|
|
|
:return: A JSON response containing the result of the `delete` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
resource = request.json["resource"]
|
|
status = database.delete_resource(resource)
|
|
database._add_to_stack({"operation": "delete", "resource": resource})
|
|
return status
|
|
|
|
|
|
@app.route("/insert", methods=["POST"])
|
|
def insert():
|
|
"""
|
|
Inserts a new resource.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session to which the data
|
|
is to be inserted and the data for inserting the resource.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is returned.
|
|
|
|
The Client API is used to insert the new resource by calling `insert_resource()` on the session where the operation is
|
|
accomplished by the concrete client class.
|
|
|
|
The `_add_to_stack` function of the session is called to insert the operation, insert, and necessary data onto the revision
|
|
operations stack.
|
|
|
|
The result of the `insert` operation is returned as a JSON response.
|
|
|
|
:return: A JSON response containing the result of the `insert` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
resource = request.json["resource"]
|
|
status = database.insert_resource(resource)
|
|
database._add_to_stack({"operation": "insert", "resource": resource})
|
|
return status
|
|
|
|
|
|
@app.route("/undo", methods=["POST"])
|
|
def undo():
|
|
"""
|
|
Undoes last operation performed on the session.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session whose last operation
|
|
is to be undone.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is returned.
|
|
|
|
The Client API is used to undo the last operation performed on the session by calling `undo_operation()` on the
|
|
session where the operation is accomplished by the concrete client class.
|
|
|
|
The result of the `undo_operation` operation is returned as a JSON response.
|
|
|
|
:return: A JSON response containing the result of the `undo_operation` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
return database.undo_operation()
|
|
|
|
|
|
@app.route("/redo", methods=["POST"])
|
|
def redo():
|
|
"""
|
|
Redoes last operation performed on the session.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session whose last operation
|
|
is to be redone.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is returned.
|
|
|
|
The Client API is used to redo the last operation performed on the session by calling `redo_operation()` on the
|
|
session where the operation is accomplished by the concrete client class.
|
|
|
|
The result of the `redo_operation` operation is returned as a JSON response.
|
|
|
|
:return: A JSON response containing the result of the `redo_operation` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
return database.redo_operation()
|
|
|
|
|
|
@app.route("/getRevisionStatus", methods=["POST"])
|
|
def get_revision_status():
|
|
"""
|
|
Gets the status of revision operations.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session whose revision operations
|
|
statuses is being requested.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is
|
|
returned.
|
|
|
|
The Client API is used to get the status of the revision operations by calling `get_revision_status()` on the
|
|
session where the operation is accomplished by the concrete client class.
|
|
|
|
The result of the `get_revision_status` is returned as a JSON response.
|
|
|
|
:return: A JSON response contain the result of the `get_revision_status` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
return database.get_revision_status()
|
|
|
|
|
|
def fernet_instance_generation(password):
|
|
"""
|
|
Generates Fernet instance for use in Saving and Loading Session.
|
|
|
|
Utilizes Scrypt Key Derivation Function with `SECRET_KEY` as salt value and recommended
|
|
values for `length`, `n`, `r`, and `p` parameters. Derives key using `password`. Derived
|
|
key is then used to initialize Fernet instance.
|
|
|
|
:param password: User provided password
|
|
:return: Fernet instance
|
|
"""
|
|
return Fernet(
|
|
base64.urlsafe_b64encode(
|
|
Scrypt(salt=app.secret_key, length=32, n=2**16, r=8, p=1).derive(
|
|
password.encode()
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
@app.route("/saveSession", methods=["POST"])
|
|
def save_session():
|
|
"""
|
|
Generates ciphertext of session that is to be saved.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session that is to be
|
|
saved and a password to be used in encrypting the session data.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is
|
|
returned.
|
|
|
|
The `save_session()` method is called to get the necessary session data from the corresponding `Client`
|
|
as a dictionary.
|
|
|
|
A Fernet instance, using the user provided password, is instantiated. The session data is encrypted using this
|
|
instance. If an Exception is raised, an error response is returned.
|
|
|
|
The result of the save_session operation is returned as a JSON response. The ciphertext is returned or an error
|
|
message if an error occurred.
|
|
|
|
:return: A JSON response containing the result of the save_session operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
session = databases[alias].save_session()
|
|
try:
|
|
fernet_instance = fernet_instance_generation(request.json["password"])
|
|
ciphertext = fernet_instance.encrypt(json.dumps(session).encode())
|
|
except (TypeError, ValueError):
|
|
return {"error": "Failed to Encrypt Session!"}, 400
|
|
return {"ciphertext": ciphertext.decode()}, 200
|
|
|
|
|
|
@app.route("/loadSession", methods=["POST"])
|
|
def load_session():
|
|
"""
|
|
Loads session from data specified in user request.
|
|
|
|
This route expects a POST request with a JSON payload containing the encrypted ciphertext containing the session
|
|
data, the alias of the session that is to be restored, and the password associated with it.
|
|
|
|
A Fernet instance, using the user provided password, is instantiated. The session data is decrypted using this
|
|
instance. If an Exception is raised, an error response is returned.
|
|
|
|
The `Client` type is retrieved from the session data and a redirect to the appropriate login with the stored
|
|
parameters from the session data is applied.
|
|
|
|
The result of the load_session operation is returned either as a JSON response containing the error message
|
|
or a redirect.
|
|
|
|
:return: A JSON response containing the error of the load_session operation or a redirect.
|
|
"""
|
|
alias = request.json["alias"]
|
|
session = request.json["session"]
|
|
try:
|
|
fernet_instance = fernet_instance_generation(request.json["password"])
|
|
session_data = json.loads(fernet_instance.decrypt(session))
|
|
except (InvalidSignature, InvalidToken):
|
|
return {"error": "Incorrect Password! Please Try Again!"}, 400
|
|
client_type = session_data["client"]
|
|
if client_type == CLIENT_TYPES[0]:
|
|
try:
|
|
databases[alias] = MongoDBClient(
|
|
mongo_uri=session_data["uri"],
|
|
database_name=session_data["database"],
|
|
collection_name=session_data["collection"],
|
|
)
|
|
except Exception as e:
|
|
return {"error": str(e)}, 400
|
|
|
|
return redirect(
|
|
url_for("editor", type=CLIENT_TYPES[0], alias=alias),
|
|
302,
|
|
)
|
|
elif client_type == CLIENT_TYPES[1]:
|
|
return redirect(
|
|
url_for("existing_json", filename=session_data["filename"]),
|
|
302,
|
|
)
|
|
else:
|
|
return {"error": "Invalid Client Type!"}, 409
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def handle404(error):
|
|
"""
|
|
Error handler for 404 (Not Found) errors.
|
|
|
|
This function is called when a 404 error occurs. It renders the "404.html" template and returns it as a response with
|
|
a status code of 404.
|
|
|
|
:param error: The error object representing the 404 error.
|
|
:return: A response containing the rendered "404.html" template with a status code of 404.
|
|
"""
|
|
return render_template("404.html"), 404
|
|
|
|
|
|
@app.route("/checkExists", methods=["POST"])
|
|
def checkExists():
|
|
"""
|
|
Checks if a resource exists based on the provided data.
|
|
|
|
This route expects a POST request with a JSON payload containing the alias of the session in which it is to be
|
|
determined whether a given resource exists and the necessary data for checking the existence of the resource.
|
|
|
|
The alias is used in retrieving the session from `databases`. If the session is not found, an error is
|
|
returned.
|
|
|
|
The Client API is used to check the existence of the resource by calling `check_resource_exists()` on the
|
|
session where the operation is accomplished by the concrete client class.
|
|
|
|
The result of the `check_resource_exists` is returned as a JSON response.
|
|
|
|
:return: A JSON response contain the result of the `check_resource_exists` operation.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
database = databases[alias]
|
|
return database.check_resource_exists(request.json)
|
|
|
|
|
|
@app.route("/logout", methods=["POST"])
|
|
def logout():
|
|
"""
|
|
Logs the user out of the application.
|
|
|
|
Deletes the alias from the `databases` dictionary.
|
|
|
|
:param alias: The alias of the database to logout from.
|
|
|
|
:return: A redirect to the index page.
|
|
"""
|
|
alias = request.json["alias"]
|
|
if alias not in databases:
|
|
return {"error": "database not found"}, 400
|
|
databases.pop(alias)
|
|
return (redirect(url_for("index")), 302)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(debug=True)
|