deleting a file using post_delete

I tried to make sure a resource is deleted, when the corresponding instance is deleted:

my model:

class SomeField(models.Model):
    name = models.CharField("name", max_length = 32, unique = True)
    
class SomeResource(models.Model):
    somefield = models.ForeignKey(SomeField, on_delete = models.CASCADE)
    file = models.FileField("the file", max_length = 255)

first approach:

def file_cleanup(sender, **kwargs):
    if Path(kwargs["instance"].file.path).is_file():
        kwargs["instance"].file.delete()

signal:

post_delete.connect(file_cleanup, sender=SomeResource, dispatch_uid="someresource.file_cleanup")

But in the database I see the instance is never deleted. The file is correctly removed. If I comment out the kwargs["instance"].file.delete() line, the instance is correctly removed. Why?

my workaround:

def file_cleanup(sender, **kwargs):
    path = Path(kwargs["instance"].file.path)
    if path.is_file():
        path.unlink()

I am trying to understand why the first “solution” did not work.

What happens if you change this from a post_delete sitgnal to a pre_delete signal?

From the docs for the post_delete signal:

instance

The actual instance being deleted.

Note that the object will no longer be in the database, so be very careful what you do with this instance.

(Note: I have no actual idea what is happening here, or what the effects may be of what you’re doing - I learned a long time ago to avoid signals except in those cases where they’re actually needed, and so I’ve never encountered anything like this.)

The delete method (you are calling) on FieldFile accepts a save parameter which defaults to True. So, in your example, the model object is saved after deleting file, thus recreating the object in database. If you pass save=False parameter when calling delete, the model object should not be recreated in database.

1 Like

Side note: when checking for existence of file, using

you suppose the storage backend is a FileSystemStorage.

If you want your code to be agnostic from backend storage (for example if you change it to S3Storage from django-storages) and still want to have your cleaning function working for such backend, you could use following test :

if kwargs["instance"].file.storage.exists(kwargs["instance"].file.name):

When trying your suggestion I end up with

TypeError file_cleanup() missing 1 required positional argument: 'sender'
def file_cleanup(sender, **kwargs):
    from .models import NewLayoutResource
    if isinstance(sender, NewLayoutResource):
        if kwargs["instance"].file.storage.exists(kwargs["instance"].file.name):
            kwargs["instance"].file.delete(save = False)

I thought using signals was smart, but after reading @KenWhitesell 's post I decided to go for:

class SomeResource(models.Model):
    somefield = models.ForeignKey(SomeField, on_delete = models.CASCADE)
    file = models.FileField("the file", max_length = 255)

    def delete_file(self):
        if (p := Path(self.file.path)).is_file():
            p.unlink()

and in the clean method of my SomeResourceForm:

def clean(self):
    clean_data = super().clean()
    if (self.has_changed() and "file" in self.initial.keys()) or clean_data["DELETE"] == True:
        self.instance.delete_file()
        return clean_data

which also works just fine and avoids all signals :slight_smile: I would probably need some time to wrap my head around pre/post_delete signals :frowning: