Skip to content

Commit

Permalink
Merge pull request #111 from MerginMaps/delete_project_by_id
Browse files Browse the repository at this point in the history
Delete project by id
  • Loading branch information
varmar05 authored Sep 18, 2023
2 parents 8b0bf44 + 64390c5 commit f88d913
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 21 deletions.
10 changes: 10 additions & 0 deletions server/mergin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ def create_app(public_keys: List[str] = None) -> Flask:
options={"swagger_ui": Configuration.SWAGGER_UI},
validate_responses=True,
)
app.add_api(
"sync/public_api_v2.yaml",
arguments={"title": "Mergin"},
options={"swagger_ui": Configuration.SWAGGER_UI},
validate_responses=True,
pythonic_params=True,
)
app.add_api(
"sync/private_api.yaml",
base_path="/app",
Expand Down Expand Up @@ -210,6 +217,9 @@ def custom_protect():
if request.path.startswith("/v1/") and "session" not in request.cookies:
# Disable csrf for non-web clients
return
if request.path.startswith("/v2/") and "session" not in request.cookies:
# Disable csrf for non-web clients
return
return csrf._protect()

csrf._protect = csrf.protect
Expand Down
7 changes: 6 additions & 1 deletion server/mergin/sync/db_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

import os
from flask import render_template, current_app, abort
from flask import current_app, abort
from sqlalchemy import event

from .. import db
Expand Down Expand Up @@ -34,13 +34,18 @@ def filter_user(ids):
)


def remove_project_storage(mapper, connection, project):
project.storage.delete()


def check(session):
if os.path.isfile(current_app.config["MAINTENANCE_FILE"]):
abort(503, "Service unavailable due to maintenance, please try later")


def register_events():
event.listen(User, "before_delete", remove_user_references)
event.listen(Project, "before_delete", remove_project_storage)
event.listen(db.session, "before_commit", check)


Expand Down
11 changes: 5 additions & 6 deletions server/mergin/sync/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,14 @@ def require_project(ws, project_name, permission):
return project


def require_project_by_uuid(uuid, permission):
def require_project_by_uuid(uuid, permission, scheduled=False):
if not is_valid_uuid(uuid):
abort(404)

project = (
Project.query.filter_by(id=uuid)
.filter(Project.removed_at.is_(None))
.first_or_404()
)
project = Project.query.filter_by(id=uuid)
if not scheduled:
project = project.filter(Project.removed_at.is_(None))
project = project.first_or_404()
workspace = project.workspace
if not workspace:
abort(404)
Expand Down
1 change: 1 addition & 0 deletions server/mergin/sync/private_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ paths:
$ref: "#/components/responses/Forbidden"
/project/removed-project/{id}:
delete:
deprecated: true
tags:
- project
- admin
Expand Down
1 change: 0 additions & 1 deletion server/mergin/sync/private_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ def force_project_delete(id): # noqa: E501
project = Project.query.get_or_404(id)
if not project.removed_at:
abort(400, "Failed to remove: Project is still active")
project.storage.delete()
db.session.delete(project)
db.session.commit()
return "", 204
Expand Down
17 changes: 9 additions & 8 deletions server/mergin/sync/public_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ paths:
- $ref: "#/components/schemas/UsersLimitHit"
x-openapi-router-controller: mergin.sync.public_api_controller
delete:
deprecated: true
tags:
- project
summary: Delete a project.
Expand All @@ -299,22 +300,22 @@ paths:
$ref: "#/components/responses/NotFoundResp"
x-openapi-router-controller: mergin.sync.public_api_controller
/project/by_uuid/{project_id}:
parameters:
- name: project_id
in: path
description: UUID of the project
required: true
schema:
type: string
get:
tags:
- project
summary: Find project specified by uuid.
description: Returns a single project with details about files including history for
versioned files (diffs) if needed.
operationId: get_project_by_uuid
parameters:
- name: project_id
in: path
description: UUID of project to return.
required: true
schema:
type: string
responses:
"200":
"204":
description: Success.
content:
application/json:
Expand Down
2 changes: 0 additions & 2 deletions server/mergin/sync/public_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@

push_triggered = signal("push_triggered")
project_version_created = signal("project_version_created")
project_deleted = signal("project_deleted")


def _project_version_files(project, version=None):
Expand Down Expand Up @@ -254,7 +253,6 @@ def delete_project(namespace, project_name): # noqa: E501
project = require_project(namespace, project_name, ProjectPermissions.Delete)
project.removed_at = datetime.utcnow()
project.removed_by = current_user.username
project_deleted.send(project)
db.session.commit()
return NoContent, 200

Expand Down
67 changes: 67 additions & 0 deletions server/mergin/sync/public_api_v2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
openapi: 3.0.0
info:
description: Mergin API to synchronize your GIS data.
version: "0.6"
title: Mergin API
servers:
- url: /v2
tags:
- name: project
description: Mergin project
paths:
/project/{id}:
delete:
tags:
- project
summary: Delete project immediately
operationId: delete_project_now
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"204":
$ref: "#/components/responses/NoContent"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
x-openapi-router-controller: mergin.sync.public_api_v2_controller
/project/{id}/scheduleDelete:
delete:
tags:
- project
summary: Schedule project to delete
operationId: schedule_delete_project
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"204":
$ref: "#/components/responses/NoContent"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
x-openapi-router-controller: mergin.sync.public_api_v2_controller
components:
responses:
NoContent:
description: No content
BadRequest:
description: Invalid request.
Unauthorized:
description: Authentication information is missing or invalid
NotFound:
description: Not found
parameters:
ProjectId:
name: id
in: path
description: UUID of the project
required: true
schema:
type: string
format: uuid
pattern: \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b
45 changes: 45 additions & 0 deletions server/mergin/sync/public_api_v2_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (C) Lutra Consulting Limited
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

from datetime import datetime

from connexion import NoContent
from flask import abort
from flask_login import current_user

from mergin import db
from mergin.auth import auth_required
from mergin.sync.models import Project
from mergin.sync.permissions import ProjectPermissions, require_project_by_uuid


@auth_required
def schedule_delete_project(id_):
"""Schedule deletion of the project.
Celery job `mergin.sync.tasks.remove_projects_backups` does the
rest.
:param id_: Project id
:type id_: str
"""
project = require_project_by_uuid(id_, ProjectPermissions.Delete)
project.removed_at = datetime.utcnow()
project.removed_by = current_user.username
db.session.commit()

return NoContent, 204


@auth_required
def delete_project_now(id_):
"""Delete the project immediately.
:param id_: Project id
:type id_: str
"""
project = require_project_by_uuid(id_, ProjectPermissions.Delete, scheduled=True)
db.session.delete(project)
db.session.commit()

return NoContent, 204
1 change: 0 additions & 1 deletion server/mergin/sync/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def remove_projects_backups():
break

for p in projects:
p.storage.delete()
db.session.delete(p)
db.session.commit()

Expand Down
2 changes: 0 additions & 2 deletions server/mergin/tests/test_db_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,4 @@ def test_remove_project(client, diff_project):
assert not Upload.query.filter_by(project_id=project_id).count()
assert not ProjectVersion.query.filter_by(project_id=project_id).count()
assert not ProjectAccess.query.filter_by(project_id=project_id).count()
# files need to be deleted manually
assert project_dir.exists()
cleanup(client, [project_dir])
39 changes: 39 additions & 0 deletions server/mergin/tests/test_public_api_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (C) Lutra Consulting Limited
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

from mergin.sync.models import Project
from tests import test_project, test_workspace_id


def test_schedule_delete_project(client):
project = Project.query.filter_by(
workspace_id=test_workspace_id, name=test_project
).first()
response = client.delete(f"v2/project/{project.id}/scheduleDelete")
assert response.status_code == 204
updated = Project.query.get(project.id)
assert updated.removed_at and updated.removed_by
response = client.delete(f"v2/project/{project.id}/scheduleDelete")
assert response.status_code == 404


def test_delete_project_now(client):
project = Project.query.filter_by(
workspace_id=test_workspace_id, name=test_project
).first()
response = client.delete(f"v2/project/{project.id}")
assert response.status_code == 204
assert not Project.query.get(project.id)
response = client.delete(f"v2/project/{project.id}")
assert response.status_code == 404


def test_delete_after_schedule(client):
project = Project.query.filter_by(
workspace_id=test_workspace_id, name=test_project
).first()
response = client.delete(f"v2/project/{project.id}/scheduleDelete")
assert response.status_code == 204
response = client.delete(f"v2/project/{project.id}")
assert response.status_code == 204

0 comments on commit f88d913

Please sign in to comment.