from django.db import IntegrityError
from django.db.models.signals import (
    pre_save,
    post_save,
    pre_delete,
    post_delete,
    m2m_changed,
)
from django.dispatch import receiver

from django.conf import settings

from ..models.authors import Author
from ..models.projects import ProjectSeries
from ..models.revisions import Revision
from ..models.artifacts import Artifact, get_deflation_directory

import logging
import os
import shutil
import functools
import tarfile

# logger for this file
logger = logging.getLogger(__name__)


# User to author linking
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def linkToAuthor(sender, **kwargs):
    """Checks if a User is already linked to an Author.
       If not, then a new Author is created and the User's
       information is copied over to the Author.
    """

    def user_is_linkable_to_author(user):
        """A user must provide enough information in order to be linkable to an Author."""
        return not (user.last_name == "" or user.first_name == "" or user.email == "")

    user_instance = kwargs["instance"]

    if not hasattr(user_instance, "author"):
        if user_is_linkable_to_author(user_instance):

            # @note(Stephan):
            # We cannot use get_or_create here since in the get we only care about
            # the email (since it is the primary key of the Author).
            if Author.objects.filter(email=user_instance.email).count() == 1:
                linked_author = Author.objects.get(email=user_instance.email)
            else:
                linked_author = Author.objects.create(
                    lastname=user_instance.last_name,
                    firstname=user_instance.first_name,
                    email=user_instance.email,
                    django_user=user_instance,
                )
            user_instance.author = linked_author
            user_instance.save()


@receiver(m2m_changed, sender=Artifact.project_series.through)
def callback_check_revision_references(sender, action, reverse, instance, **kwargs):
    """If an artifact is removed from a series, we have to check if the
       revision it belonged to still contains artifacts.

       If an artifact is added to a series, a revision is implicitely "added" to
       to this series
    """

    logger.debug("[signal artifact-serie] m2m_changed / artifact %s", instance)

    if reverse:

        changed_artifacts_pks = kwargs["pk_set"]
        project_series = instance

        # We modified the reverse relationship which means we
        # modified series.artifact.
        if action == "pre_add":
            # checking integrity for all added artifacts
            proj = project_series.project
            for pk in changed_artifacts_pks:
                artifact = Artifact.objects.get(pk=pk)
                if artifact.project != proj:
                    raise IntegrityError

        elif action == "post_remove":
            # Removing artifacts: we have to check if the
            # revision is still referenced.
            for pk in changed_artifacts_pks:
                artifact = Artifact.objects.get(pk=pk)
                artifact_revision = artifact.revision

                if (
                    artifact_revision
                    and len(artifact_revision.get_all_referencing_series()) == 0
                ):
                    artifact_revision.delete()

                if artifact.project_series.count() == 0:
                    artifact.delete()

        elif action == "post_add":
            for pk in changed_artifacts_pks:
                artifact = Artifact.objects.get(pk=pk)
                limits_artifact_numbers(artifact)

    else:
        # We modified the forward relationship which means we
        # modified artifact.project_series.
        artifact = instance

        if action == "pre_add":
            # We want to add a Series to an Artifact, we need to check that
            # this Series belongs to the same Project that the Artifact does
            proj = artifact.project
            added_series_pks = kwargs["pk_set"]
            for pk in added_series_pks:
                series = ProjectSeries.objects.get(pk=pk)
                if series.project != proj:
                    raise IntegrityError

        elif action == "post_add":
            limits_artifact_numbers(artifact)

        elif action == "post_remove":
            # clean up revision if it does not contain any artifact
            # only in case of the deletion of an artifact
            revision = artifact.revision
            if revision and len(revision.get_all_referencing_series()) == 0:
                revision.delete()

            if artifact.project_series.count() == 0:
                artifact.delete()


# Relation between Branches and Revisions
# @receiver(m2m_changed, sender=Branch.revisions.through)
# def callback_check_revision_references(sender, **kwargs):
#     """Handles changes in the Branch <--> Revision relation.

#        Enforces the revision_limit for branches, and deletes
#        non-referenced Revisions.
#     """
#     action = kwargs['action']

#     if kwargs['reverse']:
#         # We modified the reverse relationship which means we
#         # modified revision.branches.
#         # kwargs['instance'] thus always returns a revision
#         if action == 'post_add':
#             # We have added a branch to a revision. Check if the maximum
#             # revision count for this branch is violated, and if it is,
#             # delete the earliest Revisions of this branch
#             changed_objects = kwargs['pk_set']
#             for pk in changed_objects:
#                 branch = Branch.objects.get(pk=pk)
#                 enforce_revision_limit_for_branch(branch)

#         elif action == 'post_remove':
#             # We have removed a branch from a revision.
#             revision = kwargs['instance']
#             ensure_revision_references(revision)
#     else:
#         # We modified the forward relationship which means we
#         # modified branch.revisions.
#         # kwargs['instance'] thus always returns a branch
#         if action == 'post_add':
#             # We added a revision to a branch. Check if the maximum
#             # revision count is violated.
#             branch = kwargs['instance']
#             enforce_revision_limit_for_branch(branch)

#         elif action == 'post_remove':
#             # We removed a revision from a branch. Now we have to check
#             # if this removed revision is referenced by any other branch.
#             # If not, we can delete it.
#             changed_objects = kwargs['pk_set']
#             for pk in changed_objects:
#                 revision = Revision.objects.get(pk=pk)
#                 ensure_revision_references(revision)


def ensure_revision_references(revision):
    """Checks if the revision is still referenced by any branch.
       If it is not, the revision can be deleted.
    """
    if revision.branches.all().count() == 0:
        revision.delete()


def enforce_revision_limit_for_branch(branch):
    """Checks if the branch has too many Revisions. If it does,
       we remove the earliest Revisions until the desired max amount
       is reached.
    """
    while branch.revisions.count() > branch.nb_revisions_to_keep:
        revision_to_remove = branch.revisions.earliest()
        branch.revisions.remove(revision_to_remove)


def limits_artifact_numbers(artifact):
    """Contains the logic for removing the revisions from series or branches
    when their respective limit is reached"""

    # logger.debug('[signals.limits_artifact_numbers] artifact %s', artifact)

    project = artifact.project
    proj_nb_revisions_to_keep = project.nb_revisions_to_keep

    if artifact.revision is not None:
        # we are in the mode where we limit the revisions

        for serie in artifact.project_series.all():

            # these two numbers serve the same purpose
            if (
                serie.nb_revisions_to_keep is not None
                or proj_nb_revisions_to_keep is not None
            ):

                nb_revisions_limit = (
                    serie.nb_revisions_to_keep
                    if serie.nb_revisions_to_keep is not None
                    else proj_nb_revisions_to_keep
                )

                if nb_revisions_limit > 0:
                    # get all revisions of this serie
                    all_artifacts = Artifact.objects.filter(project_series=serie)

                    # we do not delete our own revision
                    all_serie_revision = (
                        Revision.objects.filter(artifacts__in=all_artifacts)
                        .distinct()
                        .order_by("commit_time")
                    )

                    nb_current_revisions = all_serie_revision.count()
                    if nb_current_revisions > nb_revisions_limit:
                        for rev_to_remove in all_serie_revision[
                            : (nb_current_revisions - nb_revisions_limit)
                        ]:
                            artifacts_to_prune = serie.artifacts.filter(
                                revision=rev_to_remove
                            ).all()
                            # logger.debug('[signals.limits_artifact_numbers] artifacts %s removed, all %s',
                            #             artifacts_to_prune.all(),
                            #             all_artifacts.all())

                            for art in artifacts_to_prune:
                                serie.artifacts.remove(art)

                            # serie.artifacts.remove(*artifacts_to_prune)

                    pass

    else:
        # we filter the number of artifacts without revision instead
        for serie in artifact.project_series.all():

            # these two numbers serve the same purpose
            if (
                serie.nb_revisions_to_keep is not None
                or proj_nb_revisions_to_keep is not None
            ):
                all_artifacts = Artifact.objects.filter(project_series=serie).order_by(
                    "upload_date"
                )

                nb_current_artifacts = all_artifacts.count()
                nb_artifacts_limit = (
                    serie.nb_revisions_to_keep
                    if serie.nb_revisions_to_keep is not None
                    else proj_nb_revisions_to_keep
                )

                if nb_current_artifacts > nb_artifacts_limit:
                    artifacts_to_prune = all_artifacts[
                        : (nb_current_artifacts - nb_artifacts_limit)
                    ]
                    # logger.debug('[signals.limits_artifact_numbers] artifacts %s removed, all %s',
                    #             artifacts_to_prune.all(),
                    #             all_artifacts.all())

                    # for art in artifacts_to_prune:
                    #    art.project_series.remove(serie)
                    serie.artifacts.remove(*artifacts_to_prune)

            pass  # if
        pass  # for

    pass  # if artifact.revision


# Artifacts
def is_deflated(instance):
    """Returns true if the artifact instance should or have been deflated"""
    return instance.is_documentation and os.path.splitext(instance.artifactfile.name)[
        1
    ] in [".tar", ".bz2", ".gz"]


# Removing deflate folder
def delete_deflate_folder(instance):
    """ Checks if the deflate folder exists, and deletes it. """

    deflate_directory = get_deflation_directory(instance)
    if os.path.exists(deflate_directory):

        def on_error(instance, function, path, excinfo):
            logger.warning(
                "[project artifact] error removing %s for instance %s", path, instance
            )
            return

        shutil.rmtree(
            deflate_directory, False, functools.partial(on_error, instance=instance)
        )


@receiver(pre_save, sender=Artifact)
def callback_artifact_field_checks(sender, instance, raw, update_fields, **kwargs):
    """Callback received before an artifact is saved"""

    logger.debug("[project artifact] pre_save artifact %s", instance)

    # we do not perform operation in case of database populating action
    if raw:
        return

    # inspects the instance in case of documentation
    if instance.is_documentation:
        if not instance.documentation_entry_file:
            raise IntegrityError(
                "Artifact has incorrect 'documentation_entry_file' field"
            )

        if instance.artifactfile.closed:
            if not tarfile.is_tarfile(instance.artifactfile.path):
                raise IntegrityError(
                    "Artifact cannot be documentation: not valid tar file"
                )
        else:
            # in this case, the file may not be yet on disk??
            import tempfile

            with tempfile.TemporaryFile() as f:

                for chunk in instance.artifactfile.chunks():
                    f.write(chunk)

                f.seek(0)
                try:
                    # same logic as tarfile.is_tarfile(tmpfile) but we have fileoj
                    tarfile.TarFile.open(fileobj=f)
                except tarfile.TarError:
                    raise IntegrityError(
                        "Artifact cannot be documentation: not valid tar file"
                    )


@receiver(post_save, sender=Artifact)
def callback_artifact_deflation_on_save(sender, instance, created, raw, **kwargs):
    """Callback received after an artifact has been saved in the database. In case of a documentation
    artifact, and in case the artifact is a zip/archive, we deflate it"""

    logger.debug("[project artifact] post_save artifact %s", instance)

    # we do not perform operation in case of database populating action
    if raw:
        return

    # If documentation, check that there is a deflate dir. If not, create it.
    # If not documentation, check that there is no deflate dir. If there is, delete it.
    deflate_directory = get_deflation_directory(instance)
    if instance.is_documentation:
        # Create if not existing
        if not os.path.exists(deflate_directory):
            os.makedirs(deflate_directory)

            # Extract
            tar = tarfile.open(fileobj=instance.artifactfile)
            tar.extractall(deflate_directory)
            instance.artifactfile.close()
    else:
        # Remove if existing
        if os.path.exists(deflate_directory):
            delete_deflate_folder(instance)

    pass


@receiver(pre_delete, sender=Artifact)
def callback_artifact_documentation_delete(sender, instance, using, **kwargs):
    """Callback received before an artifact has is being removed from the database. In case of
    a documentation artifact, and in case the artifact is a zip/archive, the deflated directory
    is removed."""
    # logger.debug('[project artifact] pre_delete artifact %s', instance)

    # deflate if documentation and archive
    if is_deflated(instance):
        delete_deflate_folder(instance)

    # removing the file on post delete
    pass


@receiver(post_delete, sender=Artifact)
def callback_artifact_delete(sender, instance, using, **kwargs):
    """Removes the artifact itself and the containing directory (if empty)"""
    # logger.debug('[project artifact] post_delete artifact %s', instance)
    storage, path = instance.artifactfile.storage, instance.artifactfile.path
    try:
        storage.delete(path)
    except (WindowsError,) as e:
        logger.warning(
            "[project artifact] error removing %s for instance %s", path, instance
        )

    parent_directory = os.path.dirname(path)
    if os.path.exists(parent_directory) and not os.listdir(parent_directory):
        logger.debug(
            "[signal][artifact][post_delete] removing empty directory %s",
            parent_directory,
        )
        try:
            os.rmdir(parent_directory)
        except (Exception,) as e:
            logger.error(
                "[signal][artifact][post_delete] failed to remote %s: %s",
                parent_directory,
                e,
            )

from django.db import IntegrityError
from django.db.models.signals import (
    pre_save,
    post_save,
    pre_delete,
    post_delete,
    m2m_changed,
)
from django.dispatch import receiver

from django.conf import settings

from ..models.authors import Author
from ..models.projects import ProjectSeries
from ..models.revisions import Revision
from ..models.artifacts import Artifact, get_deflation_directory

import logging
import os
import shutil
import functools
import tarfile

# logger for this file
logger = logging.getLogger(__name__)


# User to author linking
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def linkToAuthor(sender, **kwargs):
    """Checks if a User is already linked to an Author.
       If not, then a new Author is created and the User's
       information is copied over to the Author.
    """

    def user_is_linkable_to_author(user):
        """A user must provide enough information in order to be linkable to an Author."""
        return not (user.last_name == "" or user.first_name == "" or user.email == "")

    user_instance = kwargs["instance"]

    if not hasattr(user_instance, "author"):
        if user_is_linkable_to_author(user_instance):

            # @note(Stephan):
            # We cannot use get_or_create here since in the get we only care about
            # the email (since it is the primary key of the Author).
            if Author.objects.filter(email=user_instance.email).count() == 1:
                linked_author = Author.objects.get(email=user_instance.email)
            else:
                linked_author = Author.objects.create(
                    lastname=user_instance.last_name,
                    firstname=user_instance.first_name,
                    email=user_instance.email,
                    django_user=user_instance,
                )
            user_instance.author = linked_author
            user_instance.save()


@receiver(m2m_changed, sender=Artifact.project_series.through)
def callback_check_revision_references(sender, action, reverse, instance, **kwargs):
    """If an artifact is removed from a series, we have to check if the
       revision it belonged to still contains artifacts.

       If an artifact is added to a series, a revision is implicitely "added" to
       to this series
    """

    logger.debug("[signal artifact-serie] m2m_changed / artifact %s", instance)

    if reverse:

        changed_artifacts_pks = kwargs["pk_set"]
        project_series = instance

        # We modified the reverse relationship which means we
        # modified series.artifact.
        if action == "pre_add":
            # checking integrity for all added artifacts
            proj = project_series.project
            for pk in changed_artifacts_pks:
                artifact = Artifact.objects.get(pk=pk)
                if artifact.project != proj:
                    raise IntegrityError

        elif action == "post_remove":
            # Removing artifacts: we have to check if the
            # revision is still referenced.
            for pk in changed_artifacts_pks:
                artifact = Artifact.objects.get(pk=pk)
                artifact_revision = artifact.revision

                if (
                    artifact_revision
                    and len(artifact_revision.get_all_referencing_series()) == 0
                ):
                    artifact_revision.delete()

                if artifact.project_series.count() == 0:
                    artifact.delete()

        elif action == "post_add":
            for pk in changed_artifacts_pks:
                artifact = Artifact.objects.get(pk=pk)
                limits_artifact_numbers(artifact)

    else:
        # We modified the forward relationship which means we
        # modified artifact.project_series.
        artifact = instance

        if action == "pre_add":
            # We want to add a Series to an Artifact, we need to check that
            # this Series belongs to the same Project that the Artifact does
            proj = artifact.project
            added_series_pks = kwargs["pk_set"]
            for pk in added_series_pks:
                series = ProjectSeries.objects.get(pk=pk)
                if series.project != proj:
                    raise IntegrityError

        elif action == "post_add":
            limits_artifact_numbers(artifact)

        elif action == "post_remove":
            # clean up revision if it does not contain any artifact
            # only in case of the deletion of an artifact
            revision = artifact.revision
            if revision and len(revision.get_all_referencing_series()) == 0:
                revision.delete()

            if artifact.project_series.count() == 0:
                artifact.delete()


# Relation between Branches and Revisions
# @receiver(m2m_changed, sender=Branch.revisions.through)
# def callback_check_revision_references(sender, **kwargs):
#     """Handles changes in the Branch <--> Revision relation.

#        Enforces the revision_limit for branches, and deletes
#        non-referenced Revisions.
#     """
#     action = kwargs['action']

#     if kwargs['reverse']:
#         # We modified the reverse relationship which means we
#         # modified revision.branches.
#         # kwargs['instance'] thus always returns a revision
#         if action == 'post_add':
#             # We have added a branch to a revision. Check if the maximum
#             # revision count for this branch is violated, and if it is,
#             # delete the earliest Revisions of this branch
#             changed_objects = kwargs['pk_set']
#             for pk in changed_objects:
#                 branch = Branch.objects.get(pk=pk)
#                 enforce_revision_limit_for_branch(branch)

#         elif action == 'post_remove':
#             # We have removed a branch from a revision.
#             revision = kwargs['instance']
#             ensure_revision_references(revision)
#     else:
#         # We modified the forward relationship which means we
#         # modified branch.revisions.
#         # kwargs['instance'] thus always returns a branch
#         if action == 'post_add':
#             # We added a revision to a branch. Check if the maximum
#             # revision count is violated.
#             branch = kwargs['instance']
#             enforce_revision_limit_for_branch(branch)

#         elif action == 'post_remove':
#             # We removed a revision from a branch. Now we have to check
#             # if this removed revision is referenced by any other branch.
#             # If not, we can delete it.
#             changed_objects = kwargs['pk_set']
#             for pk in changed_objects:
#                 revision = Revision.objects.get(pk=pk)
#                 ensure_revision_references(revision)


def ensure_revision_references(revision):
    """Checks if the revision is still referenced by any branch.
       If it is not, the revision can be deleted.
    """
    if revision.branches.all().count() == 0:
        revision.delete()


def enforce_revision_limit_for_branch(branch):
    """Checks if the branch has too many Revisions. If it does,
       we remove the earliest Revisions until the desired max amount
       is reached.
    """
    while branch.revisions.count() > branch.nb_revisions_to_keep:
        revision_to_remove = branch.revisions.earliest()
        branch.revisions.remove(revision_to_remove)


def limits_artifact_numbers(artifact):
    """Contains the logic for removing the revisions from series or branches
    when their respective limit is reached"""

    # logger.debug('[signals.limits_artifact_numbers] artifact %s', artifact)

    project = artifact.project
    proj_nb_revisions_to_keep = project.nb_revisions_to_keep

    if artifact.revision is not None:
        # we are in the mode where we limit the revisions

        for serie in artifact.project_series.all():

            # these two numbers serve the same purpose
            if (
                serie.nb_revisions_to_keep is not None
                or proj_nb_revisions_to_keep is not None
            ):

                nb_revisions_limit = (
                    serie.nb_revisions_to_keep
                    if serie.nb_revisions_to_keep is not None
                    else proj_nb_revisions_to_keep
                )

                if nb_revisions_limit > 0:
                    # get all revisions of this serie
                    all_artifacts = Artifact.objects.filter(project_series=serie)

                    # we do not delete our own revision
                    all_serie_revision = (
                        Revision.objects.filter(artifacts__in=all_artifacts)
                        .distinct()
                        .order_by("commit_time")
                    )

                    nb_current_revisions = all_serie_revision.count()
                    if nb_current_revisions > nb_revisions_limit:
                        for rev_to_remove in all_serie_revision[
                            : (nb_current_revisions - nb_revisions_limit)
                        ]:
                            artifacts_to_prune = serie.artifacts.filter(
                                revision=rev_to_remove
                            ).all()
                            # logger.debug('[signals.limits_artifact_numbers] artifacts %s removed, all %s',
                            #             artifacts_to_prune.all(),
                            #             all_artifacts.all())

                            for art in artifacts_to_prune:
                                serie.artifacts.remove(art)

                            # serie.artifacts.remove(*artifacts_to_prune)

                    pass

    else:
        # we filter the number of artifacts without revision instead
        for serie in artifact.project_series.all():

            # these two numbers serve the same purpose
            if (
                serie.nb_revisions_to_keep is not None
                or proj_nb_revisions_to_keep is not None
            ):
                all_artifacts = Artifact.objects.filter(project_series=serie).order_by(
                    "upload_date"
                )

                nb_current_artifacts = all_artifacts.count()
                nb_artifacts_limit = (
                    serie.nb_revisions_to_keep
                    if serie.nb_revisions_to_keep is not None
                    else proj_nb_revisions_to_keep
                )

                if nb_current_artifacts > nb_artifacts_limit:
                    artifacts_to_prune = all_artifacts[
                        : (nb_current_artifacts - nb_artifacts_limit)
                    ]
                    # logger.debug('[signals.limits_artifact_numbers] artifacts %s removed, all %s',
                    #             artifacts_to_prune.all(),
                    #             all_artifacts.all())

                    # for art in artifacts_to_prune:
                    #    art.project_series.remove(serie)
                    serie.artifacts.remove(*artifacts_to_prune)

            pass  # if
        pass  # for

    pass  # if artifact.revision


# Artifacts
def is_deflated(instance):
    """Returns true if the artifact instance should or have been deflated"""
    return instance.is_documentation and os.path.splitext(instance.artifactfile.name)[
        1
    ] in [".tar", ".bz2", ".gz"]


# Removing deflate folder
def delete_deflate_folder(instance):
    """ Checks if the deflate folder exists, and deletes it. """

    deflate_directory = get_deflation_directory(instance)
    if os.path.exists(deflate_directory):

        def on_error(instance, function, path, excinfo):
            logger.warning(
                "[project artifact] error removing %s for instance %s", path, instance
            )
            return

        shutil.rmtree(
            deflate_directory, False, functools.partial(on_error, instance=instance)
        )


@receiver(pre_save, sender=Artifact)
def callback_artifact_field_checks(sender, instance, raw, update_fields, **kwargs):
    """Callback received before an artifact is saved"""

    logger.debug("[project artifact] pre_save artifact %s", instance)

    # we do not perform operation in case of database populating action
    if raw:
        return

    # inspects the instance in case of documentation
    if instance.is_documentation:
        if not instance.documentation_entry_file:
            raise IntegrityError(
                "Artifact has incorrect 'documentation_entry_file' field"
            )

        if instance.artifactfile.closed:
            if not tarfile.is_tarfile(instance.artifactfile.path):
                raise IntegrityError(
                    "Artifact cannot be documentation: not valid tar file"
                )
        else:
            # in this case, the file may not be yet on disk??
            import tempfile

            with tempfile.TemporaryFile() as f:

                for chunk in instance.artifactfile.chunks():
                    f.write(chunk)

                f.seek(0)
                try:
                    # same logic as tarfile.is_tarfile(tmpfile) but we have fileoj
                    tarfile.TarFile.open(fileobj=f)
                except tarfile.TarError:
                    raise IntegrityError(
                        "Artifact cannot be documentation: not valid tar file"
                    )


@receiver(post_save, sender=Artifact)
def callback_artifact_deflation_on_save(sender, instance, created, raw, **kwargs):
    """Callback received after an artifact has been saved in the database. In case of a documentation
    artifact, and in case the artifact is a zip/archive, we deflate it"""

    logger.debug("[project artifact] post_save artifact %s", instance)

    # we do not perform operation in case of database populating action
    if raw:
        return

    # If documentation, check that there is a deflate dir. If not, create it.
    # If not documentation, check that there is no deflate dir. If there is, delete it.
    deflate_directory = get_deflation_directory(instance)
    if instance.is_documentation:
        # Create if not existing
        if not os.path.exists(deflate_directory):
            os.makedirs(deflate_directory)

            # Extract
            tar = tarfile.open(fileobj=instance.artifactfile)
            tar.extractall(deflate_directory)
            instance.artifactfile.close()
    else:
        # Remove if existing
        if os.path.exists(deflate_directory):
            delete_deflate_folder(instance)

    pass


@receiver(pre_delete, sender=Artifact)
def callback_artifact_documentation_delete(sender, instance, using, **kwargs):
    """Callback received before an artifact has is being removed from the database. In case of
    a documentation artifact, and in case the artifact is a zip/archive, the deflated directory
    is removed."""
    # logger.debug('[project artifact] pre_delete artifact %s', instance)

    # deflate if documentation and archive
    if is_deflated(instance):
        delete_deflate_folder(instance)

    # removing the file on post delete
    pass


@receiver(post_delete, sender=Artifact)
def callback_artifact_delete(sender, instance, using, **kwargs):
    """Removes the artifact itself and the containing directory (if empty)"""
    # logger.debug('[project artifact] post_delete artifact %s', instance)
    storage, path = instance.artifactfile.storage, instance.artifactfile.path
    try:
        storage.delete(path)
    except (WindowsError,) as e:
        logger.warning(
            "[project artifact] error removing %s for instance %s", path, instance
        )

    parent_directory = os.path.dirname(path)
    if os.path.exists(parent_directory) and not os.listdir(parent_directory):
        logger.debug(
            "[signal][artifact][post_delete] removing empty directory %s",
            parent_directory,
        )
        try:
            os.rmdir(parent_directory)
        except (Exception,) as e:
            logger.error(
                "[signal][artifact][post_delete] failed to remote %s: %s",
                parent_directory,
                e,
            )

# logger for this file # User to author linking Checks if a User is already linked to an Author.        If not, then a new Author is created and the User's        information is copied over to the Author. A user must provide enough information in order to be linkable to an Author. # @note(Stephan): # We cannot use get_or_create here since in the get we only care about # the email (since it is the primary key of the Author). If an artifact is removed from a series, we have to check if the        revision it belonged to still contains artifacts.         If an artifact is added to a series, a revision is implicitely "added" to        to this series # We modified the reverse relationship which means we # modified series.artifact. # checking integrity for all added artifacts # Removing artifacts: we have to check if the # revision is still referenced. # We modified the forward relationship which means we # modified artifact.project_series. # We want to add a Series to an Artifact, we need to check that # this Series belongs to the same Project that the Artifact does # clean up revision if it does not contain any artifact # only in case of the deletion of an artifact # Relation between Branches and Revisions # @receiver(m2m_changed, sender=Branch.revisions.through) # def callback_check_revision_references(sender, **kwargs): #     """Handles changes in the Branch <--> Revision relation. #        Enforces the revision_limit for branches, and deletes #        non-referenced Revisions. #     """ #     action = kwargs['action'] #     if kwargs['reverse']: #         # We modified the reverse relationship which means we #         # modified revision.branches. #         # kwargs['instance'] thus always returns a revision #         if action == 'post_add': #             # We have added a branch to a revision. Check if the maximum #             # revision count for this branch is violated, and if it is, #             # delete the earliest Revisions of this branch #             changed_objects = kwargs['pk_set'] #             for pk in changed_objects: #                 branch = Branch.objects.get(pk=pk) #                 enforce_revision_limit_for_branch(branch) #         elif action == 'post_remove': #             # We have removed a branch from a revision. #             revision = kwargs['instance'] #             ensure_revision_references(revision) #     else: #         # We modified the forward relationship which means we #         # modified branch.revisions. #         # kwargs['instance'] thus always returns a branch #         if action == 'post_add': #             # We added a revision to a branch. Check if the maximum #             # revision count is violated. #             branch = kwargs['instance'] #             enforce_revision_limit_for_branch(branch) #         elif action == 'post_remove': #             # We removed a revision from a branch. Now we have to check #             # if this removed revision is referenced by any other branch. #             # If not, we can delete it. #             changed_objects = kwargs['pk_set'] #             for pk in changed_objects: #                 revision = Revision.objects.get(pk=pk) #                 ensure_revision_references(revision) Checks if the revision is still referenced by any branch.        If it is not, the revision can be deleted. Checks if the branch has too many Revisions. If it does,        we remove the earliest Revisions until the desired max amount        is reached. Contains the logic for removing the revisions from series or branches     when their respective limit is reached # logger.debug('[signals.limits_artifact_numbers] artifact %s', artifact) # we are in the mode where we limit the revisions # these two numbers serve the same purpose # get all revisions of this serie # we do not delete our own revision # logger.debug('[signals.limits_artifact_numbers] artifacts %s removed, all %s', #             artifacts_to_prune.all(), #             all_artifacts.all()) # serie.artifacts.remove(*artifacts_to_prune) # we filter the number of artifacts without revision instead # these two numbers serve the same purpose # logger.debug('[signals.limits_artifact_numbers] artifacts %s removed, all %s', #             artifacts_to_prune.all(), #             all_artifacts.all()) # for art in artifacts_to_prune: #    art.project_series.remove(serie) # if # for # if artifact.revision # Artifacts Returns true if the artifact instance should or have been deflated # Removing deflate folder Checks if the deflate folder exists, and deletes it. Callback received before an artifact is saved # we do not perform operation in case of database populating action # inspects the instance in case of documentation # in this case, the file may not be yet on disk?? # same logic as tarfile.is_tarfile(tmpfile) but we have fileoj Callback received after an artifact has been saved in the database. In case of a documentation     artifact, and in case the artifact is a zip/archive, we deflate it # we do not perform operation in case of database populating action # If documentation, check that there is a deflate dir. If not, create it. # If not documentation, check that there is no deflate dir. If there is, delete it. # Create if not existing # Extract # Remove if existing Callback received before an artifact has is being removed from the database. In case of     a documentation artifact, and in case the artifact is a zip/archive, the deflated directory     is removed. # logger.debug('[project artifact] pre_delete artifact %s', instance) # deflate if documentation and archive # removing the file on post delete Removes the artifact itself and the containing directory (if empty) # logger.debug('[project artifact] post_delete artifact %s', instance)