Delete log files on instance deletion
Delete log files from the file system when the corresponding model
instance is deleted. Add a corresponding test case.
Issue: HIC-271
Change-Id: I44709c78839091272ccc9b931e7cbe7b3947dff9
diff --git a/crashreports/models.py b/crashreports/models.py
index c54f7f1..25aa452 100644
--- a/crashreports/models.py
+++ b/crashreports/models.py
@@ -6,6 +6,7 @@
from django.db import models, transaction
from django.contrib.auth.models import User
+from django.dispatch import receiver
from taggit.managers import TaggableManager
@@ -163,6 +164,16 @@
)
+@receiver(models.signals.post_delete, sender=LogFile)
+def auto_delete_file_on_delete(sender, instance, **kwargs):
+ """Delete the file from the filesystem on deletion of the db instance."""
+ # pylint: disable=unused-argument
+
+ if instance.logfile:
+ if os.path.isfile(instance.logfile.path):
+ instance.logfile.delete(save=False)
+
+
class HeartBeat(models.Model):
"""A heartbeat that was sent by a device."""
diff --git a/crashreports/tests/test_rest_api_logfiles.py b/crashreports/tests/test_rest_api_logfiles.py
index afe8abb..9b8b6b2 100644
--- a/crashreports/tests/test_rest_api_logfiles.py
+++ b/crashreports/tests/test_rest_api_logfiles.py
@@ -12,7 +12,12 @@
from rest_framework import status
-from crashreports.models import crashreport_file_name, Device
+from crashreports.models import (
+ crashreport_file_name,
+ Device,
+ Crashreport,
+ LogFile,
+)
from crashreports.tests.utils import HiccupCrashreportsAPITestCase, Dummy
@@ -22,6 +27,7 @@
LIST_CREATE_URL = "api_v1_crashreports"
PUT_LOGFILE_URL = "api_v1_putlogfile_for_device_id"
+ POST_LOGFILE_URL = "api_v1_logfiles_by_id"
def setUp(self):
"""Call the super setup method and register a device."""
@@ -102,6 +108,30 @@
"""Test upload of logfiles as Fairphone staff user."""
self._test_logfile_upload(self.fp_staff_client, self.device_uuid)
+ def test_logfile_deletion(self):
+ """Test deletion of logfile instances."""
+ # Create a user, device and crashreport with logfile
+ device = Dummy.create_dummy_device(Dummy.create_dummy_user())
+ crashreport = Dummy.create_dummy_report(Crashreport, device)
+ logfile, logfile_path = Dummy.create_dummy_log_file_with_actual_file(
+ crashreport
+ )
+
+ # Assert that the crashreport and logfile have been created
+ self.assertEqual(Crashreport.objects.count(), 1)
+ self.assertEqual(LogFile.objects.count(), 1)
+ self.assertTrue(os.path.isfile(logfile_path))
+
+ # Delete the logfile
+ response = self.fp_staff_client.delete(
+ reverse(self.POST_LOGFILE_URL, args=[logfile.id])
+ )
+ self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
+
+ # Assert that the logfile has been deleted
+ self.assertEqual(LogFile.objects.count(), 0)
+ self.assertFalse(os.path.isfile(logfile_path))
+
def tearDown(self):
"""Remove the file and directories that were created for the test."""
shutil.rmtree(settings.MEDIA_ROOT)
diff --git a/crashreports/tests/utils.py b/crashreports/tests/utils.py
index 9ee316a..fcaa2a3 100644
--- a/crashreports/tests/utils.py
+++ b/crashreports/tests/utils.py
@@ -1,17 +1,25 @@
"""Utility functions shared by all crashreports tests."""
import os
+import shutil
import zipfile
from datetime import date, datetime
from typing import Optional
import pytz
+from django.conf import settings
from django.contrib.auth.models import User, Group
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
-from crashreports.models import Crashreport, Device, HeartBeat, LogFile
+from crashreports.models import (
+ Crashreport,
+ Device,
+ HeartBeat,
+ LogFile,
+ crashreport_file_name,
+)
from hiccup.allauth_adapters import FP_STAFF_GROUP_NAME
@@ -102,11 +110,6 @@
}
)
- DEFAULT_DUMMY_LOG_FILE_VALUES = {
- "logfile_type": "last_kmsg",
- "logfile": "test_logfile.zip",
- }
-
DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
CRASH_TYPE_TO_BOOT_REASON_MAP = {
@@ -115,12 +118,18 @@
"other": "whatever",
}
+ DEFAULT_DUMMY_LOG_FILE_FILENAME = "test_logfile.zip"
DEFAULT_DUMMY_LOG_FILE_DIRECTORY = os.path.join("resources", "test")
DEFAULT_DUMMY_LOG_FILE_PATH = os.path.join(
- DEFAULT_DUMMY_LOG_FILE_DIRECTORY, "test_logfile.zip"
+ DEFAULT_DUMMY_LOG_FILE_DIRECTORY, DEFAULT_DUMMY_LOG_FILE_FILENAME
)
+ DEFAULT_DUMMY_LOG_FILE_VALUES = {
+ "logfile_type": "last_kmsg",
+ "logfile": DEFAULT_DUMMY_LOG_FILE_FILENAME,
+ }
+
@staticmethod
def _update_copy(original, update):
"""Merge fields of update into a copy of original."""
@@ -272,6 +281,38 @@
return entity
@staticmethod
+ def create_dummy_log_file_with_actual_file(crashreport, **kwargs):
+ """Create a dummy log file instance along with a file.
+
+ The dummy instance is created and saved to the database. The log file
+ is copied to the respective location in the media directory.
+
+ Args:
+ crashreport: The crashreport that the log file belongs to.
+ **kwargs: Optional arguments to extend/overwrite the default values.
+
+ Returns: The created log file instance and the path to the copied file.
+
+ """
+ logfile = Dummy.create_dummy_log_file(crashreport, **kwargs)
+ logfile_filename = os.path.basename(logfile.logfile.path)
+ test_logfile_path = os.path.join(
+ settings.MEDIA_ROOT,
+ crashreport_file_name(logfile, logfile_filename),
+ )
+ logfile.logfile = test_logfile_path
+ logfile.save()
+
+ os.makedirs(os.path.dirname(test_logfile_path))
+ shutil.copy(
+ os.path.join(
+ Dummy.DEFAULT_DUMMY_LOG_FILE_DIRECTORY, logfile_filename
+ ),
+ test_logfile_path,
+ )
+ return logfile, test_logfile_path
+
+ @staticmethod
def read_logfile_contents(path_to_zipfile, logfile_name):
"""Read bytes of a zipped logfile."""
archive = zipfile.ZipFile(path_to_zipfile, "r")