Add migration for new logfile paths

Update all logfile paths in the database and migrate the referenced log
files to the new directory structure.

Issue: HIC-238
Change-Id: I69b53993e77079258e12ec995cbf85d3eb0a780b
diff --git a/.gitignore b/.gitignore
index f90cfcd..c19350f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -120,5 +120,6 @@
 documentation/api-endpoints.adoc
 documentation/api-endpoints.html
 
-# Crashreports uploads directory
+# Crashreport uploads directories
 crashreport_uploads/
+crashreport_uploads_legacy/
diff --git a/crashreports/migrations/0004_update_logfile_paths.py b/crashreports/migrations/0004_update_logfile_paths.py
new file mode 100644
index 0000000..fe6afac
--- /dev/null
+++ b/crashreports/migrations/0004_update_logfile_paths.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+
+"""Migrations to update the path where logfiles are stored."""
+# pylint: disable=invalid-name
+
+import logging
+import os
+import shutil
+
+from django.db import migrations
+from django.conf import settings
+
+from crashreports.models import LogFile, crashreport_file_name
+
+
+def get_django_logger():
+    """Get the Django logger instance."""
+    logger_name = next(iter(settings.LOGGING["loggers"].keys()))
+    return logging.getLogger(logger_name)
+
+
+def migrate_logfiles(apps, schema_editor):
+    """Migrate the logfiles and update the logfile paths in the database."""
+    # pylint: disable=unused-argument
+
+    logger = get_django_logger()
+
+    crashreport_uploads_dir = "crashreport_uploads"
+    crashreport_uploads_legacy_dir = "crashreport_uploads_legacy"
+    if not os.path.isdir(crashreport_uploads_dir):
+        logger.info(
+            "%s directory not found. Assuming this is a new installation and "
+            "the migration does not need to be applied.",
+            os.path.join(settings.BASE_DIR, crashreport_uploads_dir),
+        )
+        return
+
+    shutil.move(crashreport_uploads_dir, crashreport_uploads_legacy_dir)
+
+    for logfile in LogFile.objects.all():
+        migrate_logfile_instance(
+            logfile, crashreport_uploads_dir, crashreport_uploads_legacy_dir
+        )
+
+
+def migrate_logfile_instance(
+    logfile, crashreport_uploads_dir, crashreport_uploads_legacy_dir
+):
+    """Migrate a single logfile instance."""
+    logger = get_django_logger()
+
+    original_path = logfile.logfile.name
+    old_logfile_path = original_path.replace(
+        crashreport_uploads_dir, crashreport_uploads_legacy_dir, 1
+    )
+    new_logfile_path = crashreport_file_name(
+        logfile, os.path.basename(original_path)
+    )
+    logger.info("Migrating %s", old_logfile_path)
+    if os.path.isfile(old_logfile_path):
+        update_logfile_path(logfile, new_logfile_path)
+        move_logfile_file(old_logfile_path, new_logfile_path)
+    else:
+        logger.warning("Logfile does not exist: %s", old_logfile_path)
+
+
+def move_logfile_file(old_logfile_path, new_logfile_path):
+    """Move a logfile to a new path and delete empty directories."""
+    logger = get_django_logger()
+
+    logger.debug("Creating directories for %s", new_logfile_path)
+    os.makedirs(os.path.dirname(new_logfile_path), exist_ok=True)
+
+    logger.debug("Moving %s to %s", old_logfile_path, new_logfile_path)
+    shutil.move(old_logfile_path, new_logfile_path)
+
+    logger.debug("Deleting empty directories from %s", old_logfile_path)
+    os.removedirs(os.path.dirname(old_logfile_path))
+
+
+def update_logfile_path(logfile, new_logfile_path):
+    """Update the path of a logfile database instance."""
+    logger = get_django_logger()
+    logger.debug(
+        "Changing logfile path in database from %s to %s",
+        logfile.logfile,
+        new_logfile_path,
+    )
+
+    logfile.logfile = new_logfile_path
+    logfile.save()
+
+
+class Migration(migrations.Migration):
+    """Run the migration script."""
+
+    dependencies = [
+        ("crashreports", "0003_crashreport_and_heartbeat_with_radio_version")
+    ]
+
+    operations = [migrations.RunPython(migrate_logfiles)]