Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dojo/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def ready(self):
import dojo.cred.signals
import dojo.endpoint.signals
import dojo.engagement.signals
import dojo.file_uploads.signals
import dojo.finding_group.signals
import dojo.notes.signals
import dojo.product.signals
Expand Down
39 changes: 39 additions & 0 deletions dojo/db_migrations/0241_file_upload_cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.db import migrations
from django.db.models import Q


def delete_unreferenced_file_uploads(apps, schema_editor):
FileUpload = apps.get_model("dojo", "FileUpload")
# Filter files that have no relations to Finding, Test, or Engagement
unused_files = FileUpload.objects.filter(
Q(finding__isnull=True) &
Q(test__isnull=True) &
Q(engagement__isnull=True)
).distinct()
# Delete the files from disk first, then delete the FileUpload object
for file_upload in unused_files:
if file_upload.file:
storage = file_upload.file.storage
path = file_upload.file.path
if path and storage.exists(path):
storage.delete(path)
file_upload.delete()


def cannot_turn_back_time(apps, schema_editor):
"""
We cannot possibly return to the original state without knowing
the original value at the time the migration is revoked. Instead
we will do nothing.
"""
pass


class Migration(migrations.Migration):
dependencies = [
("dojo", "0240_jira_instance_password_help_text_fix"),
]

operations = [
migrations.RunPython(delete_unreferenced_file_uploads, cannot_turn_back_time),
]
2 changes: 2 additions & 0 deletions dojo/engagement/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.urls import reverse
from django.utils.translation import gettext as _

from dojo.file_uploads.helper import delete_related_files
from dojo.models import Engagement, Product
from dojo.notes.helper import delete_related_notes
from dojo.notifications.helper import create_notification
Expand Down Expand Up @@ -65,3 +66,4 @@ def engagement_post_delete(sender, instance, using, origin, **kwargs):
def engagement_pre_delete(sender, instance, **kwargs):
with contextlib.suppress(sender.DoesNotExist):
delete_related_notes(instance)
delete_related_files(instance)
Empty file added dojo/file_uploads/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions dojo/file_uploads/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import logging

logger = logging.getLogger(__name__)


def delete_related_files(obj):
if not hasattr(obj, "files"):
logger.warning(f"Attempted to delete files from object type {type(obj)} without 'files' attribute.")
return
logger.debug(f"Deleting {obj.files.count()} files for {type(obj).__name__} {obj.id}")
obj.files.all().delete()
35 changes: 35 additions & 0 deletions dojo/file_uploads/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.db.models.signals import post_delete, pre_save
from django.dispatch import receiver

from dojo.models import FileUpload


@receiver(post_delete, sender=FileUpload)
def delete_file_on_object_delete(sender, instance, **kwargs):
"""
Deletes file from filesystem when corresponding `FileUpload` is deleted.
Mostly used as a backup in case the FileUpload.delete() function fails
for whatever reason
"""
if instance.file:
storage = instance.file.storage
path = instance.file.path
if path and storage.exists(path):
storage.delete(path)


@receiver(pre_save, sender=FileUpload)
def delete_old_file_on_change(sender, instance, **kwargs):
"""Deletes old file when a new file is uploaded to the same record."""
if not instance.pk:
return # Skip new objects
try:
old_file = FileUpload.objects.get(pk=instance.pk).file
except FileUpload.DoesNotExist:
return
new_file = instance.file
if old_file and old_file != new_file:
storage = old_file.storage
path = old_file.path
if path and storage.exists(path):
storage.delete(path)
2 changes: 2 additions & 0 deletions dojo/finding/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from dojo.celery import app
from dojo.decorators import dojo_async_task, dojo_model_from_id, dojo_model_to_id
from dojo.endpoint.utils import save_endpoints_to_add
from dojo.file_uploads.helper import delete_related_files
from dojo.models import (
Endpoint,
Endpoint_Status,
Expand Down Expand Up @@ -413,6 +414,7 @@ def finding_pre_delete(sender, instance, **kwargs):
# https://code.djangoproject.com/ticket/154
instance.found_by.clear()
delete_related_notes(instance)
delete_related_files(instance)


def finding_delete(instance, **kwargs):
Expand Down
8 changes: 8 additions & 0 deletions dojo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,14 @@ class FileUpload(models.Model):
title = models.CharField(max_length=100, unique=True)
file = models.FileField(upload_to=UniqueUploadNameProvider("uploaded_files"))

def delete(self, *args, **kwargs):
"""Delete the model and remove the file from storage."""
storage = self.file.storage
path = self.file.path
super().delete(*args, **kwargs)
if path and storage.exists(path):
storage.delete(path)

def copy(self):
copy = _copy_model_util(self)
# Add unique modifier to file name
Expand Down
2 changes: 2 additions & 0 deletions dojo/test/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.urls import reverse
from django.utils.translation import gettext as _

from dojo.file_uploads.helper import delete_related_files
from dojo.models import Engagement, Finding, Product, Test
from dojo.notes.helper import delete_related_notes
from dojo.notifications.helper import create_notification
Expand Down Expand Up @@ -57,3 +58,4 @@ def update_found_by_for_findings(sender, instance, **kwargs):
def test_pre_delete(sender, instance, **kwargs):
with contextlib.suppress(sender.DoesNotExist):
delete_related_notes(instance)
delete_related_files(instance)
7 changes: 5 additions & 2 deletions dojo/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from contextlib import suppress
from pathlib import Path

from auditlog.models import LogEntry
Expand Down Expand Up @@ -157,7 +158,8 @@ def manage_files(request, oid, obj_type):

for o in files_formset.deleted_objects:
logger.debug("removing file: %s", o.file.name)
(Path(settings.MEDIA_ROOT) / o.file.name).unlink()
with suppress(FileNotFoundError):
(Path(settings.MEDIA_ROOT) / o.file.name).unlink()

for o in files_formset.new_objects:
logger.debug("adding file: %s", o.file.name)
Expand All @@ -168,7 +170,8 @@ def manage_files(request, oid, obj_type):
finding__isnull=True)
for o in orphan_files:
logger.debug("purging orphan file: %s", o.file.name)
(Path(settings.MEDIA_ROOT) / o.file.name).unlink()
with suppress(FileNotFoundError):
(Path(settings.MEDIA_ROOT) / o.file.name).unlink()
o.delete()

messages.add_message(
Expand Down