- Development setup
- Architecture overview
- Code structure
- Development workflow
- Testing
- Security guidelines
- Contributing
- Deployment
- Python 3.12
- PostgreSQL (required; SQLite is not supported since v2.2.0)
- Redis or Valkey (Celery broker/result backend and scheduler lock)
- Node.js 22.22.2 and npm (frontend build)
- FFmpeg and ImageMagick
- Git
- Clone the repository:
git clone https://github.com/ttlequals0/PixelProbe.git
cd PixelProbe- Create a virtual environment:
python3.12 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate- Install Python dependencies:
pip install -r requirements-test.txtrequirements-test.txt includes the base requirements.txt, so this one command installs both runtime and test dependencies.
- Install system dependencies:
On Ubuntu/Debian:
sudo apt-get update
sudo apt-get install -y ffmpeg imagemagick libmagic1On macOS:
brew install ffmpeg imagemagick libmagic- Build the frontend assets (required):
npm ci && npm run buildThe templates load webpack-built bundles from static/dist/; the app will not render correctly without this step.
- Set up environment variables:
cp .env.example .env
# Edit .env with your configuration- Run the development server:
python app.pyThere is no separate database initialization step: app.py runs create_tables() and any pending migrations automatically at import time.
The application will be available at http://localhost:5000
The container needs PostgreSQL and Redis/Valkey to start, so use the compose stack described in docker-setup.md rather than a bare docker run. To build a local image:
docker build --platform=linux/amd64 -t pixelprobe:dev .+-----------------+ +-----------------+ +-----------------+
| Web Client |---->| Flask API |---->| PostgreSQL DB |
+-----------------+ +-----------------+ +-----------------+
|
v
+-----------------+
| Media Scanner |
+-----------------+
|
+------+------+
v v
+---------+ +---------+
| FFmpeg | |ImageMag |
+---------+ +---------+
-
Presentation layer (
templates/,static/)- HTML templates with hand-rolled CSS and JavaScript, bundled by webpack
- Real-time progress updates
-
API layer (
pixelprobe/api/)- RESTful endpoints
- Request validation
- Rate limiting
- CSRF protection
-
Business logic layer (
pixelprobe/services/)- Scan orchestration
- Statistics calculation
- Export functionality
- Maintenance operations
-
Data access layer (
pixelprobe/repositories/)- Database operations
- Query optimization
- Transaction management
-
Core scanner (
pixelprobe/media_checker.py)- File discovery
- Corruption detection
- Multi-tool validation
The full, maintained directory tree lives in project-structure.md. This guide does not duplicate it; when the layout changes, update that document only.
- Follow PEP 8 for Python code
- Use type hints where appropriate
- Maximum line length: 100 characters
- Use meaningful variable names
Never commit directly to main. All changes go through a feature or fix branch and a pull request:
- Create a feature branch off main:
git checkout -b feature/your-feature-name- Make your changes and commit:
git add .
git commit -m "feat: add new scanning feature"- Push and create a PR:
git push origin feature/your-feature-name- Wait for both CI and CodeQL to pass on the PR before building or tagging any Docker images. Fixing a CodeQL finding after an image is built forces a rebuild and re-push of the same tag.
Follow the Conventional Commits specification:
feat:New featurefix:Bug fixdocs:Documentation changesstyle:Code style changesrefactor:Code refactoringtest:Test additions/changeschore:Maintenance tasks
- API endpoint:
# pixelprobe/api/your_routes.py
from flask import Blueprint, request, jsonify
from pixelprobe.utils.security import validate_json_input
your_bp = Blueprint('your_feature', __name__, url_prefix='/api')
@your_bp.route('/your-endpoint', methods=['POST'])
@validate_json_input({
'field': {'required': True, 'type': str}
})
def your_endpoint():
"""Your endpoint description"""
data = request.get_json()
# Implementation
return jsonify({'result': 'success'})- Register blueprint:
# app.py
from pixelprobe.api.your_routes import your_bp
app.register_blueprint(your_bp)- Add service logic:
# pixelprobe/services/your_service.py
class YourService:
def __init__(self):
pass
def process_data(self, data):
# Business logic here
return resultMigrations live in pixelprobe/migrations/startup.py and run automatically at startup, not in app.py. To add a schema change:
- Update the model:
# pixelprobe/models.py
class YourModel(db.Model):
new_field = db.Column(db.String(100))-
Add a versioned migration function in
pixelprobe/migrations/startup.py, following the existingrun_vX_Y_Z_migrations(db)pattern (for examplerun_v2_6_61_migrations). -
Register it in the
_run_all_migrations(db)registry in the same file so it runs at startup.
A PostgreSQL advisory lock coordinates migrations across multiple gunicorn workers and containers, so each migration runs exactly once per deployment.
Install the test dependencies and build the frontend assets first (some tests and the app itself expect the built static files):
pip install -r requirements-test.txt
npm ci && npm run build# Fast local run: skips tests that need the real media sample corpus
pytest -m "not real_media"
# Run with coverage
pytest -m "not real_media" --cov=pixelprobe
# Run specific test file
pytest tests/unit/test_scan_service.pyThe full test suite includes real_media tests. CI also runs them inside the Docker image, where the exact FFmpeg and ImageMagick versions match production. See testing-guide.md for the full testing reference.
- Unit test example:
# tests/unit/test_scan_service.py
import pytest
from pixelprobe.services.scan_service import ScanService
def test_scan_file_validation():
service = ScanService()
# Test invalid path
with pytest.raises(ValueError):
service.scan_file("../../../etc/passwd")
# Test valid path
result = service.scan_file("/allowed/path/image.jpg")
assert result is not None- Integration test example:
# tests/integration/test_api_endpoints.py
def test_scan_endpoint(client):
response = client.post('/api/scan-file', json={
'file_path': '/test/image.jpg'
})
assert response.status_code == 200
assert 'message' in response.jsonReal media fixtures (valid and corrupted samples per format) live in tests/fixtures/media_samples/ and are wired up by the test_data_dir fixture in tests/conftest.py.
Note: scripts/create_test_database.py is a legacy script from the SQLite era and does not work with the PostgreSQL-only application. Do not use it.
Always validate user input:
from pixelprobe.utils.security import validate_file_path, validate_json_input
# Path validation
try:
safe_path = validate_file_path(user_input)
except PathTraversalError:
return jsonify({'error': 'Invalid path'}), 400
# JSON validation decorator
@validate_json_input({
'field': {'required': True, 'type': str, 'max_length': 100}
})Always use the safe wrapper:
from pixelprobe.utils.security import safe_subprocess_run
# Safe
result = safe_subprocess_run(['ffmpeg', '-i', file_path])
# Never do this
result = subprocess.run(f'ffmpeg -i {file_path}', shell=True) # DANGEROUS!Authentication is implemented in pixelprobe/auth.py:
- Session-based login for the web UI (24-hour lifetime, 30-minute inactivity timeout)
- Bearer API tokens for programmatic access (managed via
/api/tokens) - Protect new endpoints with the
@auth_requireddecorator
- Check existing issues and PRs
- Discuss major changes in an issue first
- Update documentation for new features
- Add tests for new functionality
- Ensure all tests pass
- Update CHANGELOG.MD
- Request review from maintainers
- Code follows style guidelines
- Tests added/updated
- Documentation updated
- Security considerations addressed
- Performance impact considered
- Backward compatibility maintained
- Environment variables:
# .env.production
DEBUG=False
SECRET_KEY=your-strong-secret-key
POSTGRES_HOST=db
POSTGRES_PORT=5432
POSTGRES_DB=pixelprobe
POSTGRES_USER=pixelprobe
POSTGRES_PASSWORD=your-db-password
SCAN_PATHS=/media/photos,/media/videos
TZ=UTCThe database is configured via the individual POSTGRES_* variables; DATABASE_URL is deprecated since v2.2.0. Scan directories are set with SCAN_PATHS (comma-separated). See configuration.md for the full variable reference.
- Gunicorn configuration:
The real configuration is gunicorn.conf.py in the repository root:
GUNICORN_WORKERS- worker count (default 4)GUNICORN_TIMEOUT- worker timeout in seconds (default 300; long scans need the headroom)GUNICORN_BIND- comma-separated bind address list for dual-stack IPv4/IPv6 (default0.0.0.0:5000)GUNICORN_LOG_LEVEL- log level (defaultinfo)- Access and error logs go to stdout/stderr
There are no worker_class or max_requests settings.
- Run with Gunicorn:
gunicorn -c gunicorn.conf.py app:appBuild the production image for linux/amd64:
docker build --platform=linux/amd64 -t pixelprobe:latest .A bare docker run will not start: the application requires PostgreSQL and Redis/Valkey. Deploy with the compose stack documented in docker-setup.md.
-
Health checks:
/healthzis the unauthenticated liveness endpoint; use it for container healthchecks and uptime monitors/healthreturns status details but requires authentication- Check scan queue status
- Monitor disk space
-
Logging:
- Application logs:
/app/logs/ - Scan logs: include timestamps and file paths
- Error tracking: log all exceptions
- Application logs:
-
Performance:
- Monitor scan duration
- Track memory usage
- Database query performance
Regular backups of:
- PostgreSQL database
- Configuration files
- Scan results
- Error logs
- Test updates in staging environment
- Backup database before updates
- Run database migrations
- Monitor for issues after deployment
-
"No module named 'magic'"
- Install:
pip install python-magic - On Windows: Also need
python-magic-bin
- Install:
-
"ffmpeg not found"
- Ensure FFmpeg is in PATH
- Install with package manager
-
Database connection issues
- Check PostgreSQL service is running
- Verify the
POSTGRES_*environment variables
-
Performance problems (slow scans, memory pressure)
- See performance-tuning.md for worker sizing, batch settings, and database tuning
Enable debug logging:
# .env
DEBUG=True
LOG_LEVEL=DEBUG# Enable profiling
from werkzeug.middleware.profiler import ProfilerMiddleware
app.wsgi_app = ProfilerMiddleware(app.wsgi_app)