drives initial plan
This commit is contained in:
1 parent
75dac1e459
commit
6ad0a461e4
5 files changed
+534
No files matched your search
@@ -0,0 +1,106 @@
|
||||
# Phase 2: Storage Service
|
||||
|
||||
## Objective
|
||||
Implement the backend service layer that handles all disk operations: uploading files to hashed paths, resolving file metadata, and serving content. This module is `backend/src/drive_service.py`.
|
||||
|
||||
## Concepts
|
||||
|
||||
### Database-First Metadata
|
||||
Files are database records pointing to disk locations. Access paths are constructed lazily via a resolve function so internal path formats can change without breaking external references.
|
||||
|
||||
### Minimal Backend Layers
|
||||
Access flows through three strict stages:
|
||||
1. **Auth**: Bearer token validation.
|
||||
2. **DB Lookup**: Verify ownership (`user_id`) and Drive existence.
|
||||
3. **Serve**: If requesting raw content, pass the resolved disk path to Quart's send functions.
|
||||
|
||||
The router stays thin; the database handles all authorization before any file system calls.
|
||||
|
||||
## Tasks
|
||||
|
||||
### 2.1 Implement `drive_service.py`
|
||||
|
||||
Create `backend/src/drive_service.py` with the following core functions:
|
||||
|
||||
```python
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
from datetime import timezone
|
||||
import database as db
|
||||
|
||||
DRIVES_ROOT = None
|
||||
|
||||
async def init_drives(data_directory):
|
||||
"""Initialize DRIVES_ROOT global from app startup."""
|
||||
global DRIVES_ROOT
|
||||
DRIVES_ROOT = os.path.join(data_directory, "drives")
|
||||
os.makedirs(DRIVES_ROOT, exist_ok=True)
|
||||
|
||||
def _generate_disk_hash():
|
||||
"""Generate a random hex string for filesystem-level filenames."""
|
||||
return secrets.token_hex(8)
|
||||
|
||||
async def upload_file(user_id, drive_name, readable_filename, stream, mime_type=None):
|
||||
"""Write an incoming stream to disk and register it in the DB. Returns file ID."""
|
||||
# 1. Locate or create Drive
|
||||
drive = await db.Drive.get_or_none(user_id=user_id, name=drive_name)
|
||||
if not drive:
|
||||
drive = await db.Drive.create(user_id=user_id, name=drive_name)
|
||||
|
||||
# 2. Prepare disk path
|
||||
disk_hash = _generate_disk_hash()
|
||||
drive_dir = os.path.join(DRIVES_ROOT, str(user_id), drive_name)
|
||||
os.makedirs(drive_dir, exist_ok=True)
|
||||
file_path = os.path.join(drive_dir, disk_hash)
|
||||
|
||||
# 3. Write stream and calculate size
|
||||
size = 0
|
||||
with open(file_path, "wb") as f:
|
||||
while chunk := stream.read(8192):
|
||||
f.write(chunk)
|
||||
size += len(chunk)
|
||||
|
||||
# 4. Register in database
|
||||
record = await db.DriveFile.create(
|
||||
drive_id=drive.id,
|
||||
filename=readable_filename,
|
||||
disk_hash=disk_hash,
|
||||
mime_type=mime_type or "application/octet-stream",
|
||||
size_bytes=size,
|
||||
)
|
||||
return record.id
|
||||
|
||||
async def get_drive_file_metadata(file_id):
|
||||
"""Return JSON metadata for frontend display."""
|
||||
file = await db.DriveFile.get_or_none(id=file_id)
|
||||
if not file:
|
||||
raise Exception("File not found")
|
||||
return {
|
||||
"id": file.id,
|
||||
"name": file.filename,
|
||||
"size": file.size_bytes,
|
||||
"mime": file.mime_type,
|
||||
"created": file.created_at.isoformat(),
|
||||
"accessed": file.accessed_at.isoformat() if file.accessed_at else None
|
||||
}
|
||||
|
||||
async def resolve_drive_file_for_serving(file_id, current_user_id):
|
||||
"""Resolve file to absolute disk path after permission check. Returns (path, mime)."""
|
||||
# Eager load drive and verify ownership
|
||||
file = await db.DriveFile.filter(id=file_id).select_related("drive__user").get_or_none()
|
||||
if not file:
|
||||
raise FileNotFoundError("File not found")
|
||||
if file.drive.user_id != current_user_id:
|
||||
raise PermissionError("Access denied")
|
||||
|
||||
# Update access time asynchronously
|
||||
from asyncio import create_task
|
||||
create_task(db.DriveFile.filter(id=file_id).update(accessed_at=timezone.now()))
|
||||
|
||||
abs_path = os.path.join(DRIVES_ROOT, str(file.drive.user_id), file.drive.name, file.disk_hash)
|
||||
if not os.path.exists(abs_path):
|
||||
raise FileNotFoundError("File missing on disk")
|
||||
|
||||
return abs_path, file.mime_type
|
||||
```
|
||||
Reference in new issue
Block a user