Skip to content

Upload/Download

File upload and download functionality is essential for many web applications. SØAD provides built-in support for handling file operations securely and efficiently. This recipe covers everything from simple file uploads to advanced features like image processing and download security.


Simple File Upload

file_upload.py
from utils import render
from models import Document
import os
from java.io import File
from com.google.common.io import Files

class File_upload(object):
    def view(self, ctx):
        # Show upload form
        ctx.go_to = render.as_view(ctx, "file_upload")

    def upload(self, ctx):
        try:
            request = ctx.getRequest()

            # Get uploaded file using SØAD's built-in file handling
            file_content = request.getParameter("document")     # File content as bytes
            file_type = request.getParameter("document_ft")     # MIME type
            file_name = request.getParameter("document_fn")     # Original filename

            if file_content and file_name:
                # Validate file size (approximate check on byte array)
                file_size = len(file_content)
                if file_size > 5 * 1024 * 1024:  # 5MB limit
                    ctx.output["error"] = "File size must be less than 5MB"
                    ctx.go_to = render.as_view(ctx, "file_upload")
                    return

                # Validate file type if needed
                allowed_types = ["application/pdf", "image/jpeg", "image/png", "image/gif", 
                               "text/plain", "application/msword", 
                               "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]

                if file_type not in allowed_types:
                    ctx.output["error"] = "File type '%s' is not allowed" % file_type
                    ctx.go_to = render.as_view(ctx, "file_upload")
                    return

                # Create upload directory if it doesn't exist
                upload_dir = os.path.join(ctx.getRealPath(), "uploads", "documents")
                if not os.path.exists(upload_dir):
                    os.makedirs(upload_dir)

                # Generate safe filename to prevent directory traversal
                safe_filename = os.path.basename(file_name)  # Remove any path components
                file_path = os.path.join(upload_dir, safe_filename)

                # Save file to disk
                file_obj = File(file_path)
                Files.write(file_content, file_obj)

                # Save file info to database
                doc = Document()
                doc.set("filename", safe_filename)
                doc.set("file_path", file_path)
                doc.set("file_size", file_size)
                doc.set("content_type", file_type)

                # Get description if provided
                description = request.getParameter("description")
                if description:
                    doc.set("description", description)

                doc.saveIt()

                ctx.output["success"] = "File uploaded successfully!"
                ctx.output["document"] = doc
            else:
                ctx.output["error"] = "Please select a file to upload"

        except Exception as e:
            ctx.output["error"] = "Upload failed: " + str(e)

        ctx.go_to = render.as_view(ctx, "file_upload")
file_upload.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <h2>Upload Document</h2>

                {{#if error}}
                    <div class="alert alert-danger">
                        <i class="fas fa-exclamation-circle"></i> {{error}}
                    </div>
                {{/if}}

                {{#if success}}
                    <div class="alert alert-success">
                        <i class="fas fa-check-circle"></i> {{success}}
                        {{#if document}}
                            <div class="mt-2">
                                <strong>File Details:</strong><br>
                                <small>
                                    Name: {{document.filename}}<br>
                                    Size: {{document.file_size}} bytes<br>
                                    Type: {{document.content_type}}
                                </small>
                            </div>
                        {{/if}}
                    </div>
                {{/if}}

                <div class="card">
                    <div class="card-body">
                        <form action="/t/example/file_upload/upload" method="post" enctype="multipart/form-data">
                            <div class="mb-3">
                                <label for="document" class="form-label">Choose File</label>
                                <input type="file" class="form-control" id="document" name="document" required>
                                <div class="form-text">Maximum file size: 5MB</div>
                            </div>

                            <div class="mb-3">
                                <label for="description" class="form-label">Description (Optional)</label>
                                <textarea class="form-control" id="description" name="description" rows="3" placeholder="Enter file description..."></textarea>
                            </div>

                            <button type="submit" class="btn btn-primary">
                                <i class="fas fa-upload"></i> Upload File
                            </button>
                            <a href="/t/example/file_list" class="btn btn-secondary">
                                <i class="fas fa-list"></i> View Files
                            </a>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"></script>
</body>
</html>
CREATE TABLE document (
    id int NOT NULL AUTO_INCREMENT,
    filename varchar(255) NOT NULL,
    file_path varchar(500) NOT NULL,
    file_size bigint,
    content_type varchar(100),
    description text,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_filename (filename),
    KEY idx_created (created_at)
);

How It Works

  1. Form Setup: Use enctype="multipart/form-data" for file uploads
  2. File Retrieval: SØAD automatically extracts file data into three parameters:
  3. document → File content as byte array
  4. document_ft → MIME type (e.g., application/pdf)
  5. document_fn → Original filename (e.g., report.pdf)
  6. Validation: Check file size using len(file_content) and validate MIME type
  7. Save File: Use Java's Files.write() to save byte array to disk
  8. Database Record: Store file metadata for secure access control

Image Upload with Processing

image_upload.py
from utils import render
from models import Image
import os
from java.io import File, ByteArrayInputStream
from javax.imageio import ImageIO
from java.awt.image import BufferedImage
from java.awt import RenderingHints
from com.google.common.io import Files

class Image_upload(object):
    def view(self, ctx):
        # Show recent uploads
        recent_images = Image.findAll().orderBy("created_at DESC").limit(6)
        ctx.output["recent_images"] = recent_images
        ctx.go_to = render.as_view(ctx, "image_upload")

    def upload(self, ctx):
        try:
            request = ctx.getRequest()

            # Get uploaded image using SØAD's built-in file handling
            image_content = request.getParameter("image")      # Image content as bytes
            image_type = request.getParameter("image_ft")      # MIME type
            image_name = request.getParameter("image_fn")      # Original filename

            if image_content and image_name:
                # Validate image type
                if not image_type or not image_type.startswith("image/"):
                    ctx.output["error"] = "Please upload a valid image file"
                    ctx.go_to = render.as_view(ctx, "image_upload")
                    return

                # Validate file size (2MB for images)
                file_size = len(image_content)
                if file_size > 2 * 1024 * 1024:
                    ctx.output["error"] = "Image size must be less than 2MB"
                    ctx.go_to = render.as_view(ctx, "image_upload")
                    return

                # Create upload directories
                upload_base = os.path.join(ctx.getRealPath(), "uploads", "images")
                original_dir = os.path.join(upload_base, "original")
                thumbnail_dir = os.path.join(upload_base, "thumbnails")

                for directory in [original_dir, thumbnail_dir]:
                    if not os.path.exists(directory):
                        os.makedirs(directory)

                # Generate safe filename
                safe_filename = os.path.basename(image_name)
                original_path = os.path.join(original_dir, safe_filename)

                # Save original image
                original_file = File(original_path)
                Files.write(image_content, original_file)

                # Create thumbnail
                thumbnail_path = self.create_thumbnail(ctx, image_content, safe_filename, thumbnail_dir)

                # Save to database
                image = Image()
                image.set("filename", safe_filename)
                image.set("original_path", original_path)
                image.set("thumbnail_path", thumbnail_path)
                image.set("file_size", file_size)
                image.set("content_type", image_type)
                image.saveIt()

                ctx.output["success"] = "Image uploaded and processed successfully!"
                ctx.output["uploaded_image"] = image
            else:
                ctx.output["error"] = "Please select an image to upload"

        except Exception as e:
            ctx.output["error"] = "Upload failed: " + str(e)

        # Reload recent images
        recent_images = Image.findAll().orderBy("created_at DESC").limit(6)
        ctx.output["recent_images"] = recent_images
        ctx.go_to = render.as_view(ctx, "image_upload")

    def create_thumbnail(self, ctx, image_bytes, filename, thumbnail_dir):
        """Create a thumbnail version of the uploaded image"""
        try:
            # Read image from byte array
            input_stream = ByteArrayInputStream(image_bytes)
            original_image = ImageIO.read(input_stream)

            if original_image:
                # Calculate thumbnail dimensions (max 150x150)
                orig_width = original_image.getWidth()
                orig_height = original_image.getHeight()

                thumb_width = 150
                thumb_height = 150

                # Maintain aspect ratio
                if orig_width > orig_height:
                    thumb_height = int((float(orig_height) / orig_width) * thumb_width)
                else:
                    thumb_width = int((float(orig_width) / orig_height) * thumb_height)

                # Create thumbnail
                thumbnail = BufferedImage(thumb_width, thumb_height, BufferedImage.TYPE_INT_RGB)
                graphics = thumbnail.createGraphics()
                graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
                graphics.drawImage(original_image, 0, 0, thumb_width, thumb_height, None)
                graphics.dispose()

                # Save thumbnail
                thumb_filename = "thumb_" + filename
                thumbnail_path = os.path.join(thumbnail_dir, thumb_filename)

                # Save as JPEG for consistent format
                ImageIO.write(thumbnail, "jpg", File(thumbnail_path))
                return thumbnail_path

        except Exception as e:
            print("Thumbnail creation failed: " + str(e))

        return None
image_upload.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Upload</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .image-preview {
            max-width: 200px;
            max-height: 200px;
            object-fit: cover;
            border-radius: 8px;
        }
        .upload-area {
            border: 2px dashed #dee2e6;
            border-radius: 8px;
            padding: 2rem;
            text-align: center;
            transition: border-color 0.3s;
        }
        .upload-area:hover {
            border-color: #0d6efd;
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <h2 class="mb-4">Image Upload & Gallery</h2>

        {{#if error}}
            <div class="alert alert-danger">
                <i class="fas fa-exclamation-triangle"></i> {{error}}
            </div>
        {{/if}}

        {{#if success}}
            <div class="alert alert-success">
                <i class="fas fa-check-circle"></i> {{success}}
            </div>
        {{/if}}

        <div class="row">
            <!-- Upload Form -->
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h5 class="mb-0"><i class="fas fa-cloud-upload-alt"></i> Upload New Image</h5>
                    </div>
                    <div class="card-body">
                        <form action="/t/example/image_upload/upload" method="post" enctype="multipart/form-data">
                            <div class="upload-area mb-3">
                                <input type="file" class="form-control" id="image" name="image" accept="image/*" required onchange="previewImage(this)">
                                <div class="mt-2">
                                    <i class="fas fa-image fa-2x text-muted"></i>
                                    <p class="mb-0">Choose an image file</p>
                                    <small class="text-muted">Max size: 2MB</small>
                                </div>
                            </div>

                            <!-- Image Preview -->
                            <div id="imagePreview" class="mb-3 text-center" style="display: none;">
                                <img id="preview" class="image-preview" alt="Preview">
                            </div>

                            <button type="submit" class="btn btn-primary w-100">
                                <i class="fas fa-upload"></i> Upload Image
                            </button>
                        </form>
                    </div>
                </div>
            </div>

            <!-- Recent Uploads -->
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h5 class="mb-0"><i class="fas fa-images"></i> Recent Uploads</h5>
                    </div>
                    <div class="card-body">
                        {{#if recent_images}}
                            <div class="row g-2">
                                {{#each recent_images}}
                                <div class="col-4">
                                    <div class="position-relative">
                                        <img src="{{thumbnail_path}}" class="img-fluid rounded" alt="{{filename}}" 
                                             data-bs-toggle="modal" data-bs-target="#imageModal{{id}}" 
                                             style="cursor: pointer; height: 80px; width: 100%; object-fit: cover;">
                                        <small class="d-block text-truncate mt-1" title="{{filename}}">{{filename}}</small>
                                    </div>

                                    <!-- Modal for full size image -->
                                    <div class="modal fade" id="imageModal{{id}}" tabindex="-1">
                                        <div class="modal-dialog modal-lg">
                                            <div class="modal-content">
                                                <div class="modal-header">
                                                    <h5 class="modal-title">{{filename}}</h5>
                                                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                                                </div>
                                                <div class="modal-body text-center">
                                                    <img src="{{original_path}}" class="img-fluid" alt="{{filename}}">
                                                    <div class="mt-2">
                                                        <small class="text-muted">
                                                            Size: {{file_size}} bytes | Type: {{content_type}}<br>
                                                            Uploaded: {{created_at}}
                                                        </small>
                                                    </div>
                                                </div>
                                                <div class="modal-footer">
                                                    <a href="{{original_path}}" class="btn btn-primary" download="{{filename}}">
                                                        <i class="fas fa-download"></i> Download
                                                    </a>
                                                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                {{/each}}
                            </div>
                        {{else}}
                            <div class="text-center text-muted">
                                <i class="fas fa-images fa-3x mb-3"></i>
                                <p>No images uploaded yet</p>
                            </div>
                        {{/if}}
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"></script>
    <script>
        function previewImage(input) {
            if (input.files && input.files[0]) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    document.getElementById('preview').src = e.target.result;
                    document.getElementById('imagePreview').style.display = 'block';
                };
                reader.readAsDataURL(input.files[0]);
            }
        }
    </script>
</body>
</html>
CREATE TABLE image (
    id int NOT NULL AUTO_INCREMENT,
    filename varchar(255) NOT NULL,
    original_path varchar(500) NOT NULL,
    thumbnail_path varchar(500),
    file_size bigint,
    content_type varchar(100),
    alt_text varchar(255),
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_filename (filename),
    KEY idx_created (created_at)
);

Advanced Features

  • Image Validation: Check file type and size
  • Thumbnail Generation: Auto-create smaller versions
  • Image Preview: Show preview before upload
  • Gallery Display: View recent uploads
  • Modal Viewer: Full-size image viewing

Secure File Download

file_download.py
from utils import render
from models import Document
import os
from java.io import FileInputStream, BufferedInputStream
from java.net import URLEncoder

class File_download(object):
    def view(self, ctx):
        # List all available files
        documents = Document.findAll().orderBy("created_at DESC")
        ctx.output["documents"] = documents
        ctx.go_to = render.as_view(ctx, "file_download")

    def download(self, ctx):
        """Secure file download with access control"""
        try:
            file_id = ctx.request.getParameter("id")

            if not file_id:
                ctx.output["error"] = "File ID is required"
                ctx.go_to = render.as_view(ctx, "file_download")
                return

            # Get file record from database
            document = Document.findById(file_id)

            if not document:
                ctx.output["error"] = "File not found"
                ctx.go_to = render.as_view(ctx, "file_download")
                return

            # Verify file exists on disk
            file_path = str(document.get("file_path"))
            if not os.path.exists(file_path):
                ctx.output["error"] = "File no longer exists on server"
                ctx.go_to = render.as_view(ctx, "file_download")
                return

            # Set download headers
            response = ctx.response
            filename = str(document.get("filename"))
            content_type = str(document.get("content_type")) or "application/octet-stream"

            # Set content type and disposition
            response.setContentType(content_type)
            response.setHeader("Content-Disposition", 
                "attachment; filename=\"" + URLEncoder.encode(filename, "UTF-8") + "\"")

            # Set content length if known
            file_size = document.get("file_size")
            if file_size:
                response.setContentLength(int(file_size))

            # Stream file to client
            with open(file_path, 'rb') as file:
                output_stream = response.getOutputStream()
                buffer = bytearray(8192)  # 8KB buffer

                while True:
                    bytes_read = file.readinto(buffer)
                    if bytes_read == 0:
                        break
                    output_stream.write(buffer, 0, bytes_read)

                output_stream.flush()

            # Update download count (optional)
            download_count = document.get("download_count") or 0
            document.set("download_count", download_count + 1)
            document.set("last_downloaded", "NOW()")
            document.saveIt()

            # Don't render a view for downloads
            ctx.go_to = None

        except Exception as e:
            ctx.output["error"] = "Download failed: " + str(e)
            ctx.go_to = render.as_view(ctx, "file_download")

    def preview(self, ctx):
        """Preview file in browser (for images, PDFs, etc.)"""
        try:
            file_id = ctx.request.getParameter("id")
            document = Document.findById(file_id)

            if not document:
                ctx.response.sendError(404, "File not found")
                return

            file_path = str(document.get("file_path"))
            if not os.path.exists(file_path):
                ctx.response.sendError(404, "File not found on disk")
                return

            # Set headers for inline display
            content_type = str(document.get("content_type")) or "application/octet-stream"
            ctx.response.setContentType(content_type)
            ctx.response.setHeader("Content-Disposition", "inline")

            # Stream file
            with open(file_path, 'rb') as file:
                output_stream = ctx.response.getOutputStream()
                buffer = bytearray(8192)

                while True:
                    bytes_read = file.readinto(buffer)
                    if bytes_read == 0:
                        break
                    output_stream.write(buffer, 0, bytes_read)

                output_stream.flush()

            ctx.go_to = None

        except Exception as e:
            ctx.response.sendError(500, "Preview failed: " + str(e))
file_download.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Downloads</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="d-flex justify-content-between align-items-center mb-4">
            <h2>Available Downloads</h2>
            <a href="/t/example/file_upload" class="btn btn-primary">
                <i class="fas fa-upload"></i> Upload New File
            </a>
        </div>

        {{#if error}}
            <div class="alert alert-danger">
                <i class="fas fa-exclamation-circle"></i> {{error}}
            </div>
        {{/if}}

        {{#if documents}}
            <div class="row">
                {{#each documents}}
                <div class="col-md-6 col-lg-4 mb-4">
                    <div class="card h-100">
                        <div class="card-body d-flex flex-column">
                            <div class="mb-3">
                                {{#if (eq content_type "image/jpeg" "image/png" "image/gif")}}
                                    <i class="fas fa-image fa-2x text-primary"></i>
                                {{else if (eq content_type "application/pdf")}}
                                    <i class="fas fa-file-pdf fa-2x text-danger"></i>
                                {{else if (includes content_type "text/")}}
                                    <i class="fas fa-file-alt fa-2x text-info"></i>
                                {{else}}
                                    <i class="fas fa-file fa-2x text-secondary"></i>
                                {{/if}}
                            </div>

                            <h5 class="card-title">{{filename}}</h5>

                            <div class="card-text flex-grow-1">
                                {{#if description}}
                                    <p class="text-muted">{{description}}</p>
                                {{/if}}

                                <small class="text-muted">
                                    <strong>Size:</strong> {{formatFileSize file_size}}<br>
                                    <strong>Type:</strong> {{content_type}}<br>
                                    <strong>Uploaded:</strong> {{formatDate created_at}}
                                    {{#if download_count}}<br><strong>Downloads:</strong> {{download_count}}{{/if}}
                                </small>
                            </div>

                            <div class="mt-auto">
                                <div class="btn-group w-100" role="group">
                                    <a href="/t/example/file_download/download?id={{id}}" 
                                       class="btn btn-primary btn-sm" title="Download file">
                                        <i class="fas fa-download"></i> Download
                                    </a>

                                    {{#if (or (includes content_type "image/") (eq content_type "application/pdf"))}}
                                        <a href="/t/example/file_download/preview?id={{id}}" 
                                           class="btn btn-outline-secondary btn-sm" 
                                           target="_blank" title="Preview file">
                                            <i class="fas fa-eye"></i> Preview
                                        </a>
                                    {{/if}}
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                {{/each}}
            </div>

            <div class="mt-4">
                <div class="alert alert-light">
                    <strong>{{documents.length}}</strong> file(s) available for download
                </div>
            </div>
        {{else}}
            <div class="alert alert-info text-center">
                <i class="fas fa-folder-open fa-3x mb-3"></i>
                <h4>No files available</h4>
                <p>Upload some files to get started!</p>
                <a href="/t/example/file_upload" class="btn btn-primary">
                    <i class="fas fa-upload"></i> Upload First File
                </a>
            </div>
        {{/if}}
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"></script>
</body>
</html>
-- Add download tracking columns to document table
ALTER TABLE document ADD COLUMN download_count int DEFAULT 0;
ALTER TABLE document ADD COLUMN last_downloaded timestamp NULL;
ALTER TABLE document ADD KEY idx_downloads (download_count);

-- Sample data
INSERT INTO document (filename, file_path, file_size, content_type, description) VALUES
('user_manual.pdf', '/app/uploads/documents/user_manual.pdf', 2048576, 'application/pdf', 'Complete user manual for the application'),
('sample_data.xlsx', '/app/uploads/documents/sample_data.xlsx', 1024000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Sample Excel file with demo data'),
('company_logo.png', '/app/uploads/images/company_logo.png', 45678, 'image/png', 'Official company logo in PNG format'),
('report_template.docx', '/app/uploads/documents/report_template.docx', 98765, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'Template for monthly reports');

Security Features

  • Database Verification: Check file exists in database
  • File Existence Check: Verify file exists on disk
  • Proper Headers: Set correct content-type and disposition
  • Streaming: Efficient file streaming for large files
  • Download Tracking: Track download counts and timestamps
  • Preview Support: Safe preview for images and PDFs

Multiple File Upload

bulk_upload.py
from utils import render
from models import Document
import os
from java.io import File
from com.google.common.io import Files

class Bulk_upload(object):
    def view(self, ctx):
        ctx.go_to = render.as_view(ctx, "bulk_upload")

    def upload(self, ctx):
        """Handle multiple file uploads using SØAD's file handling"""
        uploaded_files = []
        errors = []

        try:
            request = ctx.getRequest()

            # SØAD automatically handles multiple files with same name
            # For multiple files, we need to check for each file index
            file_index = 0

            while True:
                # Try to get file at current index
                file_param = "files_%d" % file_index
                file_content = request.getParameter(file_param)
                file_type = request.getParameter("%s_ft" % file_param)
                file_name = request.getParameter("%s_fn" % file_param)

                # If no file at this index, try the standard approach
                if not file_content:
                    # For single file or first file in multiple selection
                    if file_index == 0:
                        file_content = request.getParameter("files")
                        file_type = request.getParameter("files_ft")
                        file_name = request.getParameter("files_fn")

                if not file_content or not file_name:
                    break  # No more files

                try:
                    # Validate file size
                    file_size = len(file_content)
                    if file_size > 10 * 1024 * 1024:  # 10MB per file
                        errors.append("%s: File too large (max 10MB)" % file_name)
                        file_index += 1
                        continue

                    # Create upload directory
                    upload_dir = os.path.join(ctx.getRealPath(), "uploads", "documents")
                    if not os.path.exists(upload_dir):
                        os.makedirs(upload_dir)

                    # Generate safe filename
                    safe_filename = os.path.basename(file_name)
                    file_path = os.path.join(upload_dir, safe_filename)

                    # Save file to disk
                    file_obj = File(file_path)
                    Files.write(file_content, file_obj)

                    # Save to database
                    doc = Document()
                    doc.set("filename", safe_filename)
                    doc.set("file_path", file_path)
                    doc.set("file_size", file_size)
                    doc.set("content_type", file_type)
                    doc.saveIt()

                    uploaded_files.append({
                        "filename": safe_filename,
                        "size": file_size,
                        "type": file_type,
                        "id": doc.getId()
                    })

                except Exception as e:
                    errors.append("%s: Upload failed - %s" % (file_name, str(e)))

                file_index += 1

            # Check if no files were processed
            if file_index == 0 or (not uploaded_files and not errors):
                ctx.output["error"] = "Please select at least one file to upload"
                ctx.go_to = render.as_view(ctx, "bulk_upload")
                return

            # Set output messages
            if uploaded_files:
                ctx.output["success"] = "Successfully uploaded %d file(s)" % len(uploaded_files)
                ctx.output["uploaded_files"] = uploaded_files

            if errors:
                ctx.output["errors"] = errors

        except Exception as e:
            ctx.output["error"] = "Bulk upload failed: %s" % str(e)

        ctx.go_to = render.as_view(ctx, "bulk_upload")
bulk_upload.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bulk File Upload</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .file-drop-zone {
            border: 2px dashed #dee2e6;
            border-radius: 8px;
            padding: 3rem;
            text-align: center;
            transition: all 0.3s;
            cursor: pointer;
        }
        .file-drop-zone:hover, .file-drop-zone.dragover {
            border-color: #0d6efd;
            background-color: #f8f9fa;
        }
        .file-list {
            max-height: 200px;
            overflow-y: auto;
        }
        .progress-container {
            display: none;
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <h2 class="mb-4">Bulk File Upload</h2>

        {{#if error}}
            <div class="alert alert-danger">
                <i class="fas fa-exclamation-circle"></i> {{error}}
            </div>
        {{/if}}

        {{#if success}}
            <div class="alert alert-success">
                <i class="fas fa-check-circle"></i> {{success}}

                {{#if uploaded_files}}
                    <div class="mt-3">
                        <strong>Uploaded files:</strong>
                        <ul class="mb-0 mt-2">
                            {{#each uploaded_files}}
                            <li>{{filename}} ({{size}} bytes)</li>
                            {{/each}}
                        </ul>
                    </div>
                {{/if}}
            </div>
        {{/if}}

        {{#if errors}}
            <div class="alert alert-warning">
                <i class="fas fa-exclamation-triangle"></i> Some files failed to upload:
                <ul class="mb-0 mt-2">
                    {{#each errors}}
                    <li>{{this}}</li>
                    {{/each}}
                </ul>
            </div>
        {{/if}}

        <div class="row">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">
                        <h5 class="mb-0"><i class="fas fa-cloud-upload-alt"></i> Select Multiple Files</h5>
                    </div>
                    <div class="card-body">
                        <form action="/t/example/bulk_upload/upload" method="post" enctype="multipart/form-data" id="uploadForm">
                            <div class="file-drop-zone" onclick="document.getElementById('fileInput').click()">
                                <i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
                                <h5>Drop files here or click to select</h5>
                                <p class="text-muted mb-0">You can select multiple files at once</p>
                                <small class="text-muted">Maximum 10MB per file</small>
                            </div>

                            <input type="file" id="fileInput" name="files[]" multiple style="display: none;" onchange="showSelectedFiles(this)">

                            <div id="selectedFiles" class="mt-3" style="display: none;">
                                <h6>Selected Files:</h6>
                                <div id="fileList" class="file-list border rounded p-2 bg-light"></div>
                                <div class="mt-3">
                                    <button type="submit" class="btn btn-primary">
                                        <i class="fas fa-upload"></i> Upload All Files
                                    </button>
                                    <button type="button" class="btn btn-secondary" onclick="clearFiles()">
                                        <i class="fas fa-times"></i> Clear Selection
                                    </button>
                                </div>
                            </div>

                            <div class="progress-container mt-3">
                                <div class="progress">
                                    <div class="progress-bar" role="progressbar" style="width: 0%"></div>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>

            <div class="col-md-4">
                <div class="card">
                    <div class="card-header">
                        <h6 class="mb-0"><i class="fas fa-info-circle"></i> Upload Guidelines</h6>
                    </div>
                    <div class="card-body">
                        <ul class="list-unstyled mb-0">
                            <li><i class="fas fa-check text-success"></i> Maximum 10MB per file</li>
                            <li><i class="fas fa-check text-success"></i> Multiple files supported</li>
                            <li><i class="fas fa-check text-success"></i> Drag and drop enabled</li>
                            <li><i class="fas fa-check text-success"></i> All file types accepted</li>
                        </ul>

                        <hr>

                        <div class="mt-3">
                            <a href="/t/example/file_download" class="btn btn-outline-primary btn-sm w-100">
                                <i class="fas fa-list"></i> View Uploaded Files
                            </a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"></script>
    <script>
        function showSelectedFiles(input) {
            const fileList = document.getElementById('fileList');
            const selectedFiles = document.getElementById('selectedFiles');

            if (input.files.length > 0) {
                let html = '';
                for (let i = 0; i < input.files.length; i++) {
                    const file = input.files[i];
                    const sizeKB = Math.round(file.size / 1024);
                    html += `
                        <div class="d-flex justify-content-between align-items-center py-1 border-bottom">
                            <div>
                                <strong>${file.name}</strong><br>
                                <small class="text-muted">${sizeKB} KB - ${file.type || 'Unknown type'}</small>
                            </div>
                            <i class="fas fa-file text-primary"></i>
                        </div>
                    `;
                }
                fileList.innerHTML = html;
                selectedFiles.style.display = 'block';
            } else {
                selectedFiles.style.display = 'none';
            }
        }

        function clearFiles() {
            document.getElementById('fileInput').value = '';
            document.getElementById('selectedFiles').style.display = 'none';
        }

        // Drag and drop functionality
        const dropZone = document.querySelector('.file-drop-zone');

        dropZone.addEventListener('dragover', function(e) {
            e.preventDefault();
            this.classList.add('dragover');
        });

        dropZone.addEventListener('dragleave', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');
        });

        dropZone.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');

            const fileInput = document.getElementById('fileInput');
            fileInput.files = e.dataTransfer.files;
            showSelectedFiles(fileInput);
        });

        // Form submission progress
        document.getElementById('uploadForm').addEventListener('submit', function() {
            document.querySelector('.progress-container').style.display = 'block';
            // Note: Real progress tracking would require AJAX
        });
    </script>
</body>
</html>

Bulk Upload Features

  • Multiple Selection: HTML5 multiple file input
  • Drag & Drop: Modern file drop interface
  • File Preview: Show selected files before upload
  • Individual Validation: Check each file separately
  • Partial Success: Continue even if some files fail
  • Error Reporting: Detailed error messages per file

Key Takeaways

  • Security First: Always validate file types, sizes, and paths
  • Error Handling: Provide clear feedback for upload failures
  • User Experience: Show progress and preview capabilities
  • Performance: Stream large files to avoid memory issues
  • Database Integration: Store file metadata for easy retrieval
  • Access Control: Verify permissions before allowing downloads