- Add /documents router with webhook, upload, search, health endpoints - Create DocumentSyncService for indexing documents to vectors/graph - Add PaperlessClient for REST API integration - Configure dependency injection for Paperless client - Add document models for webhook payloads and responses - Event-driven architecture via Paperless workflow webhooks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
510 lines
20 KiB
Markdown
510 lines
20 KiB
Markdown
# Phase 3: Document Storage System - Implementation Plan
|
|
|
|
## Overview
|
|
|
|
Document storage tier for Library Desk - storing and indexing PDFs, images, videos, and git documentation mirrors.
|
|
|
|
**User Decisions:**
|
|
- Paperless-ngx container for OCR
|
|
- Ebooks deferred to future phase
|
|
- Video.js player deferred to after core implementation
|
|
|
|
| Phase | Status | Version |
|
|
|-------|--------|---------|
|
|
| Phase 1: Cleanup System | Complete | v1.4.0 |
|
|
| Phase 2: Volatile Memory | Complete | v1.4.3 |
|
|
| Phase 3: Document Storage | Planning | - |
|
|
| Phase 4: Test Data Cleanup | Complete | v1.4.4 |
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
**Paperless-ngx as primary document store** (no SeaweedFS needed):
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ External Sources │
|
|
│ ┌─────────┐ ┌────────────┐ ┌──────────────┐ │
|
|
│ │ GitHub │ │ Direct │ │ Email/Folder │ │
|
|
│ │ Docs │ │ Upload │ │ Ingestion │ │
|
|
│ └────┬────┘ └─────┬──────┘ └──────┬───────┘ │
|
|
└───────┼─────────────┼────────────────┼──────────────────────────┘
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Paperless-ngx │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ - Document storage (PDFs, images, videos) │ │
|
|
│ │ - OCR via Tesseract (PDFs, images) │ │
|
|
│ │ - Web UI for browsing/tagging │ │
|
|
│ │ - REST API for integration │ │
|
|
│ └─────────────────────────┬─────────────────────────────────┘ │
|
|
└────────────────────────────┼────────────────────────────────────┘
|
|
│ REST API (sync)
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Library Desk │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ DocumentSyncService │ │
|
|
│ │ - Polls Paperless for new/updated docs │ │
|
|
│ │ - Extracts text + metadata via API │ │
|
|
│ │ - Sends to vector/graph pipelines │ │
|
|
│ └─────────────────────────┬─────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ┌────────────────┼────────────────┐ │
|
|
│ ▼ ▼ ▼ │
|
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
│ │ Qdrant │ │ Neo4j │ │ Wiki.js │ │
|
|
│ │ (vectors)│ │ (graph) │ │ (catalog)│ │
|
|
│ └──────────┘ └──────────┘ └──────────┘ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**File handling by type:**
|
|
|
|
| File Type | Paperless | Library Desk |
|
|
|-----------|-----------|--------------|
|
|
| PDFs | OCR → text | Index text → vectors/graph |
|
|
| Images | OCR → text | Index text → vectors/graph |
|
|
| Videos | Storage only | Index metadata → vectors/graph |
|
|
|
|
---
|
|
|
|
## Technology Stack
|
|
|
|
| Component | Purpose | Rationale |
|
|
|-----------|---------|-----------|
|
|
| **Paperless-ngx** | Document storage + OCR | All-in-one: storage, OCR, web UI, REST API |
|
|
| **ClamAV** | Virus scanning | Host OS install, pyclamd integration, better isolation |
|
|
| **PDF.js** | PDF viewer | Embeddable in Wiki.js (deferred) |
|
|
|
|
**Why Paperless-ngx as primary store:**
|
|
- Eliminates need for separate blob storage (SeaweedFS/MinIO)
|
|
- Built-in web UI for browsing and tagging
|
|
- Tesseract OCR with 100+ language support
|
|
- REST API for Library Desk integration
|
|
- Handles videos as raw files (no OCR, but stored)
|
|
- Email and folder watching for automatic ingestion
|
|
- Active community, well-maintained
|
|
|
|
---
|
|
|
|
## Paperless-ngx API Deep Dive
|
|
|
|
### Authentication
|
|
```
|
|
POST /api/token/
|
|
Body: {"username": "...", "password": "..."}
|
|
Response: {"token": "..."}
|
|
|
|
Header: Authorization: Token <token>
|
|
```
|
|
|
|
### Document Upload (for HybridRAG → Paperless)
|
|
```
|
|
POST /api/documents/post_document/
|
|
Content-Type: multipart/form-data
|
|
|
|
Fields:
|
|
- document (file, required)
|
|
- title (string)
|
|
- created (datetime)
|
|
- correspondent (ID)
|
|
- document_type (ID)
|
|
- storage_path (ID)
|
|
- tags (repeatable IDs)
|
|
- custom_fields (JSON array)
|
|
|
|
Response: {"task_id": "uuid"}
|
|
```
|
|
|
|
Track consumption: `GET /api/tasks/?task_id={uuid}` → returns document ID when complete
|
|
|
|
### Document Search
|
|
```
|
|
GET /api/documents/?query=search+terms # Full-text search
|
|
GET /api/documents/?more_like_id=123 # Similarity search
|
|
|
|
Response includes __search_hit__:
|
|
{
|
|
"score": 0.95,
|
|
"highlights": "<span>matched</span> text",
|
|
"rank": 0
|
|
}
|
|
```
|
|
|
|
### Custom Field Filtering
|
|
```
|
|
GET /api/documents/?custom_field_query=field_name__operation=value
|
|
|
|
Operations:
|
|
- exact, in, isnull, exists (all types)
|
|
- icontains, istartswith, iendswith (text)
|
|
- gt, gte, lt, lte, range (numeric/date)
|
|
- contains (document links)
|
|
```
|
|
|
|
### Bulk Operations
|
|
```
|
|
POST /api/documents/bulk_edit/
|
|
{
|
|
"documents": [1, 2, 3],
|
|
"method": "add_tag|remove_tag|set_correspondent|set_document_type|merge|split|...",
|
|
"parameters": {...}
|
|
}
|
|
```
|
|
|
|
### Webhooks (Push to Library Desk!)
|
|
Paperless workflows can trigger webhooks on document events:
|
|
|
|
| Trigger | When | Available Data |
|
|
|---------|------|----------------|
|
|
| Consumption Started | Before OCR | file_path, source, filename |
|
|
| Document Added | After OCR | content, tags, doc_type, correspondent, `{doc_url}` |
|
|
| Document Updated | On change | Same as Added |
|
|
| Scheduled | Time-based | Date offsets from document dates |
|
|
|
|
**Webhook Action**: POST to Library Desk endpoint with document data
|
|
|
|
### Organization Features
|
|
|
|
| Feature | Purpose | API Endpoint |
|
|
|---------|---------|--------------|
|
|
| Tags | Nested labels (5 levels deep) | `/api/tags/` |
|
|
| Correspondents | Source/destination | `/api/correspondents/` |
|
|
| Document Types | Classification | `/api/document_types/` |
|
|
| Storage Paths | File organization | `/api/storage_paths/` |
|
|
| Custom Fields | Extensible metadata | `/api/custom_fields/` |
|
|
|
|
### Custom Fields We Should Create
|
|
| Field Name | Type | Purpose |
|
|
|------------|------|---------|
|
|
| `source_url` | URL | Original download URL (for HybridRAG uploads) |
|
|
| `library_indexed` | Boolean | Sync status with Library Desk |
|
|
| `library_doc_id` | Text | Library Desk document reference |
|
|
| `collection` | Text | Logical grouping (e.g., "fastapi-docs") |
|
|
|
|
### External LLM Add-ons (Optional)
|
|
Community tools exist for Ollama integration:
|
|
- **[paperless-ai](https://github.com/clusterzx/paperless-ai)** - Auto-tagging, RAG chat
|
|
- **[paperless-gpt](https://github.com/icereed/paperless-gpt)** - LLM-enhanced OCR, auto-titling
|
|
|
|
**Recommendation:** Skip these - Library Desk already has Ollama integration for:
|
|
- Embedding (nomic-embed-text)
|
|
- LLM analysis (mistral-nemo)
|
|
- Entity extraction
|
|
- HybridRAG
|
|
|
|
We'll do our own classification/tagging via Library Desk after sync.
|
|
|
|
---
|
|
|
|
## Virus Scanning Integration
|
|
|
|
**ClamAV daemon + pyclamd** (no third-party REST wrappers):
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ File Upload │────►│ Library Desk │────►│ ClamAV Daemon │
|
|
│ (URL or file) │ │ (pyclamd) │ │ (clamd:3310) │
|
|
└─────────────────┘ └────────┬────────┘ └─────────────────┘
|
|
│
|
|
┌────────────┴────────────┐
|
|
▼ ▼
|
|
┌──────────┐ ┌──────────┐
|
|
│ Clean │ │ Infected │
|
|
│ ✓ │ │ ✗ │
|
|
└────┬─────┘ └────┬─────┘
|
|
│ │
|
|
▼ ▼
|
|
Upload to Paperless Reject + Log
|
|
```
|
|
|
|
### ClamAV Deployment (Host OS)
|
|
|
|
ClamAV runs on the host OS (not containerized) for better security isolation:
|
|
|
|
```bash
|
|
# Installed via apt on Ubuntu/Debian
|
|
# Config: /etc/clamav/clamd.conf
|
|
# TCPSocket 3310
|
|
# TCPAddr 0.0.0.0
|
|
```
|
|
|
|
Benefits: scans outside container isolation, single virus DB, survives container restarts.
|
|
|
|
### Library Desk Integration
|
|
```python
|
|
# src/clients/clamav_client.py
|
|
import pyclamd
|
|
|
|
class ClamAVClient:
|
|
def __init__(self, host: str, port: int = 3310):
|
|
self.cd = pyclamd.ClamdNetworkSocket(host, port)
|
|
|
|
async def scan_bytes(self, data: bytes) -> ScanResult:
|
|
"""Scan file bytes, return clean/infected status."""
|
|
result = self.cd.scan_stream(data)
|
|
if result is None:
|
|
return ScanResult(clean=True)
|
|
return ScanResult(clean=False, virus_name=result['stream'][1])
|
|
|
|
def ping(self) -> bool:
|
|
"""Health check."""
|
|
return self.cd.ping()
|
|
```
|
|
|
|
### Scan Points
|
|
| Location | When | Action on Infected |
|
|
|----------|------|-------------------|
|
|
| `/documents/upload` | Before Paperless upload | Reject with 400, log threat |
|
|
| HybridRAG web fetch | Before saving PDF | Skip file, log threat |
|
|
| `/documents/webhook` | Optional re-scan | Quarantine in Paperless |
|
|
|
|
### Config Settings
|
|
```python
|
|
# src/config.py
|
|
CLAMAV_HOST: str = "192.168.86.149" # Host OS IP (not container)
|
|
CLAMAV_PORT: int = 3310
|
|
CLAMAV_ENABLED: bool = True # Bypass for testing
|
|
CLAMAV_TIMEOUT: int = 30 # seconds
|
|
```
|
|
|
|
---
|
|
|
|
## Integration Strategy
|
|
|
|
### Option A: Webhook Push (Preferred)
|
|
```
|
|
Paperless Workflow → POST webhook → Library Desk /documents/webhook
|
|
```
|
|
- Real-time indexing when documents added/updated
|
|
- Configure in Paperless: Workflow → Document Added → Webhook Action
|
|
- Library Desk receives document ID, fetches content via API
|
|
|
|
### Option B: Polling Pull (Fallback)
|
|
```
|
|
Scheduler → POST /documents/sync → Library Desk polls Paperless
|
|
```
|
|
- Periodic sync for missed webhooks or initial bulk import
|
|
- Track `library_indexed` custom field to skip already-processed docs
|
|
|
|
### Option C: HybridRAG Upload (New!)
|
|
```
|
|
HybridRAG web search → finds PDF → POST to Paperless → webhook → indexed
|
|
```
|
|
- When HybridRAG finds a relevant PDF/document in web results
|
|
- Download and upload to Paperless with `source_url` custom field
|
|
- Paperless OCRs it, triggers webhook, Library Desk indexes
|
|
|
|
---
|
|
|
|
## Library Desk API Design
|
|
|
|
### Documents Router (`/documents`)
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/documents/webhook` | POST | Receive Paperless webhook (Document Added/Updated) |
|
|
| `/documents/sync` | POST | Pull new/updated docs from Paperless → index |
|
|
| `/documents/upload` | POST | Upload file to Paperless (for HybridRAG) |
|
|
| `/documents/sync-from-git` | POST | Pull docs from Gitea → upload to Paperless → index |
|
|
| `/documents/{document_id}` | GET | Get document metadata |
|
|
| `/documents/{document_id}/text` | GET | Get extracted text |
|
|
| `/documents/search` | POST | Semantic search across documents |
|
|
| `/documents/collection/{name}` | GET | List documents in collection |
|
|
| `/documents/collection/{name}/catalog` | POST | Generate wiki catalog page |
|
|
|
|
**Upload flow (HybridRAG → Paperless):**
|
|
1. HybridRAG finds PDF in web results
|
|
2. POST `/documents/upload` with URL or file
|
|
3. Library Desk downloads, uploads to Paperless with metadata
|
|
4. Returns task_id for async tracking
|
|
5. Paperless webhook triggers indexing when OCR complete
|
|
|
|
### Viewers Router (`/viewers`) - Deferred
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/viewers/pdf/{document_id}` | GET | Serve PDF.js viewer |
|
|
| `/viewers/image/{document_id}` | GET | Serve image lightbox |
|
|
| `/viewers/video/{document_id}` | GET | Serve Video.js player |
|
|
|
|
---
|
|
|
|
## Data Flow: Document Processing Pipeline
|
|
|
|
```
|
|
1. INTAKE (Paperless-ngx handles this)
|
|
└─ Upload via Paperless UI, email, or folder watch
|
|
└─ Paperless assigns document ID and stores file
|
|
|
|
2. OCR EXTRACTION (Paperless-ngx handles this)
|
|
├─ PDFs → Tesseract → Plain text
|
|
├─ Images → Tesseract → Plain text
|
|
└─ Videos → Metadata only (no OCR)
|
|
|
|
3. SYNC TO LIBRARY DESK (scheduled or manual)
|
|
└─ Poll Paperless API for new/updated documents
|
|
└─ Fetch text content + metadata
|
|
|
|
4. TEXT CHUNKING
|
|
└─ VectorService._chunk_text() (existing)
|
|
|
|
5. EMBEDDING
|
|
└─ OllamaClient.embed() (existing)
|
|
|
|
6. VECTOR STORAGE (Qdrant)
|
|
└─ Payload: {doc_type: "document", paperless_id, ...}
|
|
|
|
7. GRAPH STORAGE (Neo4j)
|
|
└─ Document node + MENTIONS relationships
|
|
|
|
8. WIKI CATALOG (optional)
|
|
└─ Auto-generate catalog page via ConsolidationService
|
|
```
|
|
|
|
---
|
|
|
|
## Git Docs Integration
|
|
|
|
Extends existing `scheduler/src/executors/doc_sync_executor.py`:
|
|
|
|
1. **Scheduler** syncs docs from GitHub → Gitea (existing)
|
|
2. **Post-sync hook** calls `POST /documents/sync-from-git`
|
|
3. **Library Desk** indexes docs into vectors/graph
|
|
4. **Auto-generate** wiki catalog page for collection
|
|
|
|
---
|
|
|
|
## Wiki.js Viewer Integration
|
|
|
|
Since Wiki.js v2 requires disabled HTML sanitization for iframes:
|
|
|
|
```markdown
|
|
<!-- In wiki catalog page -->
|
|
## Document Preview
|
|
|
|
<iframe
|
|
src="http://library-desk:8089/viewers/pdf/abc123"
|
|
width="100%" height="600px">
|
|
</iframe>
|
|
```
|
|
|
|
**Wiki.js Settings Required:**
|
|
- `Administration > Security > Allowed HTML Elements: iframe`
|
|
- `Content Security Policy: frame-src http://library-desk:8089`
|
|
|
|
---
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase 3.1: Infrastructure Setup
|
|
- [ ] Deploy Paperless-ngx container (Docker Compose)
|
|
- [x] ClamAV installed on host OS (port 3310)
|
|
- [ ] Configure Paperless: storage path, OCR settings, API token
|
|
- [ ] Create custom fields in Paperless: `source_url`, `library_indexed`, `library_doc_id`, `collection`
|
|
- [ ] Create `src/clients/paperless_client.py`
|
|
- [ ] Create `src/clients/clamav_client.py` (pyclamd wrapper)
|
|
- [ ] Create `src/models/document.py`
|
|
- [ ] Add config settings to `src/config.py` (PAPERLESS_*, CLAMAV_*)
|
|
|
|
### Phase 3.2: Webhook Integration (Push)
|
|
- [ ] Create `src/routers/documents.py`
|
|
- [ ] Implement `/documents/webhook` endpoint (receives Paperless events)
|
|
- [ ] Configure Paperless Workflow: Document Added → Webhook → Library Desk
|
|
- [ ] Create `src/services/document_sync_service.py`
|
|
- [ ] Implement document indexing pipeline (fetch text → chunk → embed → graph)
|
|
|
|
### Phase 3.3: Polling Sync (Pull Fallback)
|
|
- [ ] Implement `/documents/sync` endpoint
|
|
- [ ] Poll Paperless for docs where `library_indexed=false`
|
|
- [ ] Track sync state (last_sync timestamp in Redis)
|
|
- [ ] Update `library_indexed` after successful indexing
|
|
|
|
### Phase 3.4: HybridRAG Upload Integration
|
|
- [ ] Implement `/documents/upload` endpoint
|
|
- [ ] Download file from URL
|
|
- [ ] **Virus scan before upload** (reject if infected, log threat)
|
|
- [ ] Upload clean files to Paperless with metadata
|
|
- [ ] Set `source_url` custom field
|
|
- [ ] Extend HybridRAG service to detect and upload relevant PDFs
|
|
- [ ] Add `save_to_documents` option to HybridRAG config
|
|
|
|
### Phase 3.5: Indexing Pipeline
|
|
- [ ] Extend VectorService for `doc_type: "document"`
|
|
- [ ] Extend GraphService for Document nodes (link to Paperless ID)
|
|
- [ ] Implement `/documents/search` endpoint
|
|
- [ ] Add dependency injection
|
|
|
|
### Phase 3.6: Git Docs Integration
|
|
- [ ] Create `src/clients/gitea_client.py`
|
|
- [ ] Implement `/documents/sync-from-git` → bulk upload to Paperless
|
|
- [ ] Create collection auto-cataloging (wiki pages)
|
|
- [ ] Add scheduler task for periodic git sync
|
|
|
|
### Phase 3.7: Viewers (Deferred)
|
|
*After core implementation is working*
|
|
- [ ] Create `static/pdf-viewer.html` (PDF.js)
|
|
- [ ] Create `static/image-viewer.html`
|
|
- [ ] Create `static/video-player.html` (Video.js)
|
|
- [ ] Create `src/routers/viewers.py`
|
|
|
|
### Phase 3.8: Maintenance & Testing
|
|
- [ ] Extend cleanup for document orphans
|
|
- [ ] Add document orphan detection (Paperless deleted but still in Qdrant/Neo4j)
|
|
- [ ] Create `tests/test_document_sync.py`
|
|
- [ ] Create `tests/test_paperless_client.py`
|
|
|
|
---
|
|
|
|
## Files to Create
|
|
|
|
| Path | Purpose |
|
|
|------|---------|
|
|
| `src/clients/paperless_client.py` | Paperless-ngx REST API client |
|
|
| `src/clients/clamav_client.py` | ClamAV scanner (pyclamd wrapper) |
|
|
| `src/clients/gitea_client.py` | Gitea repo access |
|
|
| `src/models/document.py` | Document/Collection/ScanResult models |
|
|
| `src/services/document_sync_service.py` | Sync orchestrator |
|
|
| `src/routers/documents.py` | Document endpoints (webhook, sync, upload, search) |
|
|
| `tests/test_document_sync.py` | Sync service tests |
|
|
| `tests/test_paperless_client.py` | API client tests |
|
|
| `tests/test_clamav_client.py` | Virus scanner tests |
|
|
| `docker/docker-compose.documents.yml` | Paperless + ClamAV deployment |
|
|
|
|
**Deferred files (Phase 3.7):**
|
|
|
|
| Path | Purpose |
|
|
|------|---------|
|
|
| `src/routers/viewers.py` | Viewer endpoints |
|
|
| `static/pdf-viewer.html` | PDF.js viewer |
|
|
| `static/image-viewer.html` | Image lightbox |
|
|
| `static/video-player.html` | Video.js player |
|
|
|
|
## Files to Modify
|
|
|
|
| Path | Changes |
|
|
|------|---------|
|
|
| `src/config.py` | `PAPERLESS_*`, `CLAMAV_*` settings |
|
|
| `src/core/dependencies.py` | DocumentSyncService, PaperlessClient, ClamAVClient DI |
|
|
| `src/main.py` | Register documents router |
|
|
| `src/services/vector_service.py` | `doc_type: "document"` handling |
|
|
| `src/services/graph_service.py` | Document node with Paperless ID |
|
|
| `src/services/hybrid_rag_service.py` | Add `save_to_documents` option + virus scan |
|
|
| `src/models/hybrid_rag.py` | Add `save_to_documents` config |
|
|
| `src/routers/maintenance.py` | Document orphan cleanup, ClamAV health check |
|
|
| `requirements.txt` | Add `pyclamd` |
|
|
|
|
## Paperless Custom Fields Setup
|
|
|
|
Create these in Paperless UI (Administration → Custom Fields):
|
|
|
|
| Field | Type | Purpose |
|
|
|-------|------|---------|
|
|
| `source_url` | URL | Original download URL |
|
|
| `library_indexed` | Boolean | Sync status |
|
|
| `library_doc_id` | Text | Library Desk reference |
|
|
| `collection` | Text | Logical grouping |
|