pythondjangofilefile-system-storage

Trigger my function when any file is saved in django


I would like to tap on file save event inside my Django application

I have dockerized Django application. When I press save in any file, the server is restarted and docker image rebuild.

I would like similar functionality: Whenever I press save in any file, I would like that my function is called after server is rebooted.

How to achieve this behaviour.

I try with overriding FileSystemStorage but it's not working.

from django.core.files.storage import FileSystemStorage
    

class CustomFileSystemStorage(FileSystemStorage):
    def _save(self, name, content):
        saved_file_name = super()._save(name, content)
        self.my_custom_function(saved_file_name, name, content)
    
        return saved_file_name

    def my_custom_function(self, file_path, name, content):
        print('File saved at: ', file_path)
        print('File name: ', name)
        print('File content: ', content)

settings.py:

DEFAULT_FILE_STORAGE = 'myApp.storage.CustomFileSystemStorage'

When I save file nothing is logged

Not a solution:

Edit:

By press save on ant file I mean press cmd+s on a file that I was editing (for example: test.py)

Any change on disk results in rebuilding the docker image. I would like to do similar that they did.

EDIT 2:

I thought that because on save server is restarted and docker image rebuild... maybe there is a way to tap on those triggers.

Can I trigger my function when server is restarted? But there would probably be difficult to get a file that was changed...

EDIT 3:

My apps.py

from django.apps import AppConfig
import logging


class MyAppConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'myApp'

    def ready(self):
        logger = logging.getLogger(__name__)
        logger.info("READY 123123")

I also try

from django.apps import AppConfig
import logging


class MyAppConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'myApp'

    def ready(self):
        print('TEST')

The settings.py

"""
Django settings for WisdomCore project.

Generated by 'django-admin startproject' using Django 4.2.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xxx'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'my_app'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'Projects.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'Project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'


LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django': {
            'level': 'INFO',
            'handlers': ['console'],
            'propagate': True,
        },
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': True,
        }
    },
}

The logs are:

Logs

I don't have any other code...

My tree

├── Dockerfile
├── Project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── docker-compose.yml
├── my_app
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── install.sh
├── local_settings.py
├── makefile
├── manage.py
├── neo4j-conf
│   └── neo4j.conf
├── neo4j_data
└── requirements.txt

Solution

  • Thank you all for the great suggestions that lead me to the correct answer.

    Detect server restart

    Similar to @Daviid suggestion, I solve this by AppConfig

    1. Create a management command that contains the code you would like to run

      # commands/startup_actions.py
      from django.core.management.base import BaseCommand
      
      class Command(BaseCommand):
          help = 'Actions to perform on server startup or app ready'
      
          def handle(self, *args, **options):
              # Put your startup actions here, similar to what you would have done in AppConfig.ready
              print("Server is starting up or app is ready.")
      
    2. Call this method inside AppConfig:

      from django.apps import AppConfig
      
      
      class MyAppConfig(AppConfig):
          default_auto_field = 'django.db.models.BigAutoField'
          name = 'myApp'
      
          def ready(self):
              # Run the custom management command on server startup
              from django.core.management import call_command
              call_command('startup_actions')
      

    This actually works and prints "Server is starting up or app is ready." on file save.

    But: You need to figure out which file has changed by yourselves.

    Detect file change

    You can use the watchdog library to detect which file has been changed

    1. Pip install watchdog:

      pip install watchdog==3.0.0
      

    also put it inside requirement.txt

    1. Create a file_observer.py file on the root directory

      # file_observer.py
      
      import time
      from watchdog.observers import Observer
      from watchdog.events import FileSystemEventHandler, RegexMatchingEventHandler
      
      from graph_parser.parser import CypherMethodParser
      
      
      class MyHandler(RegexMatchingEventHandler):
          def on_modified(self, event):
              if event.is_directory:
                  return
              # This function will be called when a file is modified
              print(f"File {event.src_path} has been modified with {event} event.")
              # Sometimes this is called 2 times in a row. To prevent this put sleep
              time.sleep(1)
      
      def start_observer():
          event_handler = MyHandler(regexes=[r'.*\.py$', r'.*\.csv$'])
          observer = Observer()
          observer.schedule(event_handler, path='.', recursive=True)
          observer.start()
          try:
              while True:
                  time.sleep(1)
          except KeyboardInterrupt:
              observer.stop()
          observer.join()
      
      
      if __name__ == "__main__":
          start_observer()
      
    2. Inside the install.sh (or where you install requirements.txt in your docker container...) add

      pip install --upgrade pip
      pip install -r requirements.txt
      python file_observer.py &        # <--- this is the new line