Skip to content

Latest commit

 

History

History
468 lines (346 loc) · 12.1 KB

File metadata and controls

468 lines (346 loc) · 12.1 KB

VaultDocs Development Session - May 2, 2026

Documents Blueprint - Complete Implementation


Session Overview

Date: May 2, 2026
Developer: Renilson Jr.
Focus: Documents Blueprint - Upload, Download, Serve, Quota Limits
Repository: https://github.com/renilsonjr/vaultdocs
Branch: main


What We Built Today

1. Storage Service Abstraction

File: app/services/storage_service.py

Classes:

  • StorageService (ABC) - Abstract base class defining the interface
  • LocalStorageService - Local filesystem implementation
  • AzureBlobService - Stub for future Azure integration
  • StorageError - Custom exception

Key Methods:

  • upload(file, tenant_id, node_id, filename) → Returns storage_path
  • delete(storage_path) → Removes file from storage
  • get_download_url(storage_path, expires_in_seconds) → Generates download URL
  • sanitize_filename(filename) → Security: prevents directory traversal

Security Features:

  • secure_filename() from werkzeug - prevents ../../etc/passwd attacks
  • Multi-tenant isolation via directory structure: {base}/{tenant}/{node}/{file}
  • Relative path storage in database for backend portability

2. Documents Blueprint

Files Created:

  • app/blueprints/documents/routes.py - Upload & download endpoints
  • app/blueprints/documents/schemas.py - Marshmallow validation
  • app/blueprints/documents/__init__.py - Blueprint package

Endpoints:

POST /api/v1/documents (Upload)

  • Accepts multipart/form-data with file + metadata
  • Validates MIME type (pdf, images, docx, xlsx, txt)
  • Enforces file size limits by plan:
    • Free: 25 MB
    • Professional: 100 MB
    • Enterprise: 500 MB
  • Checks permissions (admin/owner can upload)
  • Creates document node in database
  • Returns 201 with document details

GET /api/v1/documents/:id/download (Get Download URL)

  • Validates document exists and belongs to user's tenant
  • Generates time-limited download URL (1 hour expiry)
  • Returns download_url and expires_at
  • Returns 200 with URL details

Business Logic:

  • Parent node validation (cannot upload to document node)
  • Tenant isolation (cross-tenant access blocked)
  • Soft delete awareness (ignores deleted documents)
  • Original filename preserved in metadata

3. Files Blueprint (File Serving)

Files Created:

  • app/blueprints/files/routes.py - File serving endpoint
  • app/blueprints/files/__init__.py - Empty (blueprint package marker)

Endpoint:

GET /api/v1/files/:tenant_id/:node_id/:filename (Serve File)

  • Public endpoint (no JWT required - security via obscure UUIDs)
  • Uses Flask's send_file() to serve file bytes
  • Auto-detects Content-Type from file extension
  • Sets proper headers for browser display/download

Security Model:

  • Download endpoint validates permissions before giving URL
  • File serving URL contains UUIDs (hard to guess)
  • In production: will use Azure SAS tokens with expiration

4. Database Schema Updates

Model: app/models/node.py

New Fields Added:

storage_path = db.Column(db.String(500), nullable=True)  # Relative path
file_size = db.Column(db.BigInteger, nullable=True)      # Bytes (changed from Integer)
mime_type = db.Column(db.String(100), nullable=True)     # e.g., 'application/pdf'

Why BigInteger:

  • Integer max: ~2.1 GB (too small)
  • BigInteger max: ~9 exabytes (future-proof)

5. Configuration Updates

File: app/config.py

Added Settings:

# Storage Backend
STORAGE_BACKEND = 'local'  # or 'azure'
STORAGE_BASE_PATH = '/tmp/vaultdocs-uploads'  # macOS compatible

# File Size Limits (per upload)
MAX_FILE_SIZE_FREE = 25 * 1024 * 1024        # 25 MB
MAX_FILE_SIZE_PRO = 100 * 1024 * 1024        # 100 MB
MAX_FILE_SIZE_ENTERPRISE = 500 * 1024 * 1024  # 500 MB

# Free Plan Quotas
STORAGE_QUOTA_FREE = 3 * 1024 * 1024 * 1024  # 3 GB total
DOCUMENT_LIMIT_FREE = 50                      # 50 documents max

6. Flask App Initialization

File: app/__init__.py

Added:

# Initialize Storage Service
from app.services.storage_service import LocalStorageService, AzureBlobService
if app.config['STORAGE_BACKEND'] == 'azure':
    app.storage = AzureBlobService(...)
else:
    app.storage = LocalStorageService(
        base_path=app.config['STORAGE_BASE_PATH']
    )

# Register Blueprints
from .blueprints.documents.routes import documents_bp
from .blueprints.files.routes import files_bp
app.register_blueprint(documents_bp)
app.register_blueprint(files_bp)

7. Testing (6 Tests Passing)

File: tests/integration/test_documents_api.py

Tests:

  1. test_upload_pdf_success - Upload works correctly
  2. test_download_document_success - Download URL generated
  3. test_serve_file_success - File bytes served correctly
  4. test_upload_no_file_returns_400 - Error: no file
  5. test_upload_disallowed_mime_type_returns_400 - Error: wrong MIME
  6. test_upload_to_document_node_returns_422 - Error: invalid parent

Run All Tests:

./.venv/bin/python -m pytest tests/integration/test_documents_api.py -v

🚧 8. Quota Limits (IN PROGRESS)

Status: Implementation started, tests being written

Goal: Enforce Free plan limits:

  • Storage quota: 3 GB total
  • Document count: 50 documents max

Code Location: app/blueprints/documents/routes.py (quota checks in upload endpoint)

Tests to Complete:

  • test_upload_free_plan_storage_quota_exceeded
  • test_upload_free_plan_document_count_exceeded

Allowed MIME Types

ALLOWED_MIME_TYPES = {
    'application/pdf',
    'image/jpeg',
    'image/png',
    'image/webp',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',  # docx
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',        # xlsx
    'text/plain'
}

Complete Upload/Download Flow

User uploads file via UI ↓ POST /api/v1/documents (multipart/form-data) ↓ Backend validates:

File present & not empty MIME type allowed File size within plan limit User has permission (admin/owner) Parent node valid (not a document) Quota not exceeded (Free plan only) ↓

Backend processes:

Sanitize filename (security) Upload to storage: /tmp/vaultdocs-uploads/{tenant}/{node}/{file} Create Node record in database Store metadata (original_filename, etc.) ↓

Return 201 with document details ↓ User clicks "Download" in UI ↓ GET /api/v1/documents/{id}/download ↓ Backend validates:

Document exists Belongs to user's tenant Not soft-deleted ↓

Return 200 with download_url (storage_path) and expires_at ↓ Browser/App requests file ↓ GET /api/v1/files/{tenant}/{node}/{filename} ↓ Backend serves file bytes with correct Content-Type


File Structure

vaultdocs/ ├── app/ │ ├── init.py # Storage init + blueprint registration │ ├── config.py # Storage config added │ ├── models/ │ │ └── node.py # BigInteger file_size + storage fields │ ├── services/ │ │ ├── init.py │ │ └── storage_service.py # NEW - Storage abstraction │ ├── blueprints/ │ │ ├── documents/ # NEW │ │ │ ├── init.py │ │ │ ├── routes.py # Upload + download endpoints │ │ │ └── schemas.py # Validation │ │ └── files/ # NEW │ │ ├── init.py │ │ └── routes.py # File serving │ └── ... ├── tests/ │ └── integration/ │ └── test_documents_api.py # NEW - 6 tests passing └── ...


Git Commits (Session History)

# Commit 1
git commit -m "feat(documents): implement file upload with storage abstraction"
# - Storage service (LocalStorageService, AzureBlobService)
# - Upload endpoint with validation
# - Node model updates (storage_path, file_size, mime_type)

# Commit 2
git commit -m "feat(documents): add download endpoint and validation tests"
# - Download URL endpoint
# - Error validation tests (no file, wrong MIME, invalid parent)

# Commit 3
git commit -m "feat(documents): add file serving endpoint"
# - Files blueprint for serving bytes
# - GET /api/v1/files/:tenant/:node/:filename

Key Learning Points

1. Abstract Base Classes (ABC)

from abc import ABC, abstractmethod

class StorageService(ABC):
    @abstractmethod
    def upload(self, file, ...):
        pass  # Subclasses MUST implement

Why: Creates a contract - guarantees all storage backends have the same methods.


2. Strategy Pattern

# Can swap implementations without changing business logic:
if config['STORAGE_BACKEND'] == 'azure':
    app.storage = AzureBlobService()
else:
    app.storage = LocalStorageService()

# Business code doesn't care which one:
app.storage.upload(file, ...)  # Works for both!

3. Security - Filename Sanitization

from werkzeug.utils import secure_filename

# Attack attempt:
filename = "../../etc/passwd"

# After sanitization:
safe = secure_filename(filename)  # → "etc_passwd"

Prevents: Directory traversal attacks.


4. Multi-Tenancy Isolation

# Directory structure:
/tmp/vaultdocs-uploads/
  ├── tenant-uuid-1/
  │   └── node-uuid-a/
  │       └── invoice.pdf
  └── tenant-uuid-2/
      └── node-uuid-b/
          └── contract.pdf

Why: Tenant A cannot access Tenant B's files (filesystem-level isolation).


5. Relative vs Absolute Paths

# Database stores:
storage_path = "tenant-123/node-456/file.pdf"  # ✅ Relative

# NOT:
storage_path = "/tmp/vaultdocs-uploads/tenant-123/..."  # ❌ Absolute

Why: When migrating to Azure, path changes but relative part stays same.


6. BigInteger for Large Numbers

# Wrong (max ~2.1 GB):
file_size = db.Column(db.Integer)

# Right (max ~9 exabytes):
file_size = db.Column(db.BigInteger)

Common Issues & Solutions

Issue 1: "dquote>" in Terminal

Cause: Multi-line commit message with unescaped quotes
Solution: Use single-line messages or git commit (opens editor)

Issue 2: Indentation Errors in VS Code

Solution: Select lines, press Cmd + ] to indent right, Cmd + [ to indent left

Issue 3: /mnt Read-Only on macOS

Solution: Use /tmp/vaultdocs-uploads instead

Issue 4: Import Errors in __init__.py

Solution: Keep blueprint __init__.py files empty (they're just package markers)


Next Steps (When You Resume)

Option A: Complete Quota Limits (CURRENT - 10 minutes left)

  • Finish implementing quota checks
  • Run and fix the 2 quota tests
  • Commit quota implementation

Option B: List Documents (15 minutes)

  • GET /api/v1/nodes/:id/children - List docs in folder
  • Pagination support

Option C: Delete Documents (10 minutes)

  • DELETE /api/v1/documents/:id
  • Soft delete + file removal

Option D: More Tests (15 minutes)

  • File size limits per plan
  • Reader permission denied
  • Cross-tenant access blocked

Resume Commands

cd /Users/renilsonjr/vaultdocs
source .venv/bin/activate

# Run all document tests
pytest tests/integration/test_documents_api.py -v

# Check uploaded files
ls -lh /tmp/vaultdocs-uploads/

# Start dev server
flask run

Resume Phrase for New Chat

"Let's continue VaultDocs - Documents blueprint. We just finished file upload/download/serving (6 tests passing). I'm currently implementing quota limits. Let me share the checkpoint file."

Then upload this file: SESSION_CHECKPOINT_2026-05-02.md


Important Notes

  • Database: PostgreSQL (vaultdocs_test recreated with BigInteger)
  • Storage: Local filesystem at /tmp/vaultdocs-uploads/
  • Tests: 6 passing, 2 in progress (quota limits)
  • Git: All work committed and pushed to main
  • Learning Style: Manual typing, detailed explanations, TDD workflow

Session Complete! Great work today! 🎉