File Uploads in Lback Framework

Handling file uploads is a fundamental part of many web applications. Lback provides a robust and flexible way to manage file uploads, including validation and secure storage.

—

1. How File Uploads Work in Lback

The file upload process relies on several core components working seamlessly together:

  • BodyParsingMiddleware: (in lback/middlewares/body_parsing_middleware.py)

    This middleware is responsible for parsing the request body when the Content-Type is multipart/form-data. It transforms uploaded files into UploadedFile objects and makes them accessible via request.files.

  • UploadedFile Class: (in lback/core/types.py)

    This object represents an uploaded file. It holds useful properties such as filename (the original file name), content_type (MIME type), size (size in bytes), and file (a file-like object representing the file’s content).

  • file_handlers.py Utilities: (in lback/utils/file_handlers.py)

    This file contains helper functions to streamline file processing: * validate_uploaded_file(): Checks the file’s type and size against specified rules. * save_uploaded_file(): Saves the uploaded file to a structured directory on the file system. * delete_saved_file(): Deletes a previously saved file.

  • Generic Admin Panel (and your custom controllers):

    These components (like the Generic Add View or Generic Edit View) leverage the file_handlers functions to automatically process files uploaded from HTML forms.

The Workflow:

  1. When a user submits an HTML form containing an <input type="file"> field, the request is sent as multipart/form-data.

  2. The BodyParsingMiddleware intercepts and parses this request, extracting the uploaded files.

  3. Each uploaded file is converted into an UploadedFile object and stored in request.files (which is a MultiDict, allowing for multiple files under the same field name).

  4. In your View or Controller (whether it’s a Generic Admin View or your custom view): * You can access uploaded files via request.files. * You can call validate_uploaded_file() to check the file’s integrity before saving. * You can call save_uploaded_file() to store the file in the correct location on the server. * The saved file paths (relative paths) are what you should store in your database.

—

2. File Upload Configuration Options in settings.py

You can customize file upload behavior by configuring the following settings in your project’s settings.py file:

  • UPLOAD_FOLDER: (Optional, default is uploads) The name of the base directory within your project where all uploaded files will be stored.
    • Example: UPLOAD_FOLDER = 'static/uploads'

  • UPLOAD_ALLOWED_TYPES: (Optional, default is None which allows all types) A list of allowed MIME types for uploaded files.
    • Example: UPLOAD_ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']

  • UPLOAD_MAX_SIZE_MB: (Optional, default is None for no size limit) The maximum allowed size for uploaded files in megabytes.
    • Example: UPLOAD_MAX_SIZE_MB = 5 (maximum size of 5 MB)

—

3. How to Handle Uploaded Files in Views

Once BodyParsingMiddleware parses the request, uploaded files will be available in request.files as UploadedFile objects.

#### Examples:

  1. Accessing a Single Uploaded File: If you expect a single file for a specific field (e.g., profile_picture):

    # In your View or Controller
    from lback.core.types import Request, UploadedFile
    from lback.utils.file_handlers import save_uploaded_file, validate_uploaded_file
    from lback.core.config import Config # Ensure Config or current_app.config is imported
    
    def upload_profile_picture(request: Request):
        # Assume you have a Config object available here, perhaps from request.app.config
        config: Config = request.app.config # Or the appropriate way to access app configuration
    
        # Access the uploaded file
        profile_pic_file: Optional[UploadedFile] = request.files.get('profile_picture')
    
        if profile_pic_file:
            # Pass the `model_name` under which you want to save the files (typically the model/entity name)
            # and the validation settings defined in your config
            saved_path = save_uploaded_file(
                profile_pic_file,
                config=config,
                model_name='users', # E.g., 'users' folder within UPLOAD_FOLDER
                allowed_types=config.UPLOAD_ALLOWED_TYPES,
                max_size_mb=config.UPLOAD_MAX_SIZE_MB
            )
    
            if saved_path:
                # File saved successfully!
                # You can now store 'saved_path' in your database (e.g., 'profile_picture_path' column in the User table)
                # user.profile_picture_path = saved_path
                # db_session.add(user)
                # db_session.commit()
                request.session['flash_messages'].append({'message': f"Profile picture uploaded successfully: {saved_path}", 'category': 'success'})
                return redirect('/profile')
            else:
                # Save failed, check for validation errors
                error_message = validate_uploaded_file(profile_pic_file, config.UPLOAD_ALLOWED_TYPES, config.UPLOAD_MAX_SIZE_MB)
                if not error_message: # If no specific validation error, the error was during the save operation
                    error_message = "Failed to save profile picture due to an unexpected error."
                request.session['flash_messages'].append({'message': error_message, 'category': 'error'})
                return redirect('/upload-form')
        else:
            request.session['flash_messages'].append({'message': 'No profile picture file was provided.', 'category': 'warning'})
            return redirect('/upload-form')
    
  2. Handling Multiple Files for the Same Field ( multiple attribute in HTML): If you have a multi-file input field (e.g., gallery_images[]):

    # In your View or Controller
    from typing import List
    # ... (same previous imports) ...
    
    def upload_gallery_images(request: Request):
        config: Config = request.app.config
    
        # request.files.getlist() returns a list of objects for a given field name
        gallery_files: List[UploadedFile] = request.files.getlist('gallery_images')
    
        uploaded_paths = []
        errors = []
    
        if gallery_files:
            for file_obj in gallery_files:
                if isinstance(file_obj, UploadedFile):
                    saved_path = save_uploaded_file(
                        file_obj,
                        config=config,
                        model_name='gallery', # 'gallery' folder
                        allowed_types=config.UPLOAD_ALLOWED_TYPES,
                        max_size_mb=config.UPLOAD_MAX_SIZE_MB
                    )
                    if saved_path:
                        uploaded_paths.append(saved_path)
                        # Now you can save 'saved_path' to your database, perhaps in an Image table related to Gallery
                    else:
                        error_message = validate_uploaded_file(file_obj, config.UPLOAD_ALLOWED_TYPES, config.UPLOAD_MAX_SIZE_MB)
                        if not error_message:
                            error_message = f"Failed to save {file_obj.filename} due to an unexpected error."
                        errors.append(f"Error with {file_obj.filename}: {error_message}")
                else:
                    errors.append(f"Unexpected file object type for {file_obj.filename}.")
        else:
            errors.append("No gallery images were provided.")
    
        if uploaded_paths:
            request.session['flash_messages'].append({'message': f"Successfully uploaded {len(uploaded_paths)} images.", 'category': 'success'})
        if errors:
            for err in errors:
                request.session['flash_messages'].append({'message': err, 'category': 'error'})
    
        return redirect('/gallery')
    
  3. Deleting a Saved File:

    # In your View or Controller
    from lback.utils.file_handlers import delete_saved_file
    # ... (same previous imports) ...
    
    def delete_image(request: Request):
        image_relative_path = request.form.get('image_path') # Assume the path comes from the form
        config: Config = request.app.config
    
        if image_relative_path:
            if delete_saved_file(image_relative_path, config=config):
                # File successfully deleted from the file system
                # Now, delete the path from your database
                # image_record = db_session.query(Image).filter_by(path=image_relative_path).first()
                # if image_record:
                #     db_session.delete(image_record)
                #     db_session.commit()
                request.session['flash_messages'].append({'message': "Image deleted successfully.", 'category': 'success'})
                return redirect('/admin/images')
            else:
                request.session['flash_messages'].append({'message': "Failed to delete image from server.", 'category': 'error'})
                return redirect('/admin/images')
        else:
            request.session['flash_messages'].append({'message': "No image path provided for deletion.", 'category': 'warning'})
            return redirect('/admin/images')