End-to-end webhook flow is proven on this NC instance (NodeCreated + NodeWritten both fired and dispatched scan_folder on a PUT test), so the watchfiles-based polling layer is no longer needed. - scanner.start_initial_scan no longer queues watch_folders on boot. - scan.watch_folders kept as a one-line no-op shim so any leftover apply_async in flight from the previous deploy doesn't crash a worker. Will be deleted entirely after the queue drains. - celery.py reroutes watch_folders to the `default` queue (worker-light) so the no-op shim actually completes — the `watcher` queue is dead. - docker-compose drops the mulita-worker-watcher service. Its celery --beat responsibility (firing discard_missing_photos_beat every 30 min) moves to worker-light's command. Latency note: NC dispatches webhook events through its background-job queue, currently run by cron */5. After this commit lands you'll want to tighten cron to */1 so new uploads land in mule within ~60s instead of up to 5 min.
94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""
|
|
Scanner service for initial library scan and per-user source root bootstrap.
|
|
"""
|
|
import os
|
|
import logging
|
|
from sqlalchemy import select
|
|
|
|
from app.database import AsyncSessionLocal
|
|
from app.models import SourceRoot
|
|
from app.models.user import User
|
|
from app.tasks.scan import scan_all_source_roots
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def bootstrap_user_source_root(user: User, session=None) -> None:
|
|
"""Create the media directory and a source root for a user.
|
|
|
|
Called when a new user is created (by the admin or the setup endpoint).
|
|
If the user already has a source root, this is a no-op.
|
|
"""
|
|
own_session = session is None
|
|
if own_session:
|
|
session = AsyncSessionLocal()
|
|
|
|
try:
|
|
# Check if user already has a source root
|
|
result = await session.execute(
|
|
select(SourceRoot).where(SourceRoot.user_id == user.id)
|
|
)
|
|
if result.scalar_one_or_none() is not None:
|
|
return
|
|
|
|
os.makedirs(user.media_path, exist_ok=True)
|
|
|
|
source_root = SourceRoot(
|
|
name=f"{user.username}'s Library",
|
|
path=user.media_path,
|
|
user_id=user.id,
|
|
)
|
|
session.add(source_root)
|
|
if own_session:
|
|
await session.commit()
|
|
else:
|
|
await session.flush()
|
|
|
|
logger.info(
|
|
f"Bootstrapped source root for user '{user.username}': "
|
|
f"{user.media_path}"
|
|
)
|
|
finally:
|
|
if own_session:
|
|
await session.close()
|
|
|
|
|
|
async def bootstrap_default_source_root() -> None:
|
|
"""Legacy bootstrap — for existing installs that have source roots
|
|
without user_id (pre-auth migration). On fresh installs, source roots
|
|
are created per-user via bootstrap_user_source_root. If there are
|
|
already source roots in the DB, this is a no-op.
|
|
"""
|
|
async with AsyncSessionLocal() as session:
|
|
result = await session.execute(select(SourceRoot))
|
|
if result.scalars().first() is not None:
|
|
return # Already have source roots.
|
|
|
|
# No source roots and no users means fresh install — the setup
|
|
# endpoint will create the first user + source root.
|
|
user_count = (await session.execute(
|
|
select(User)
|
|
)).scalars().first()
|
|
if user_count is None:
|
|
logger.info(
|
|
"No users or source roots — waiting for first-run setup."
|
|
)
|
|
return
|
|
|
|
|
|
async def start_initial_scan():
|
|
"""Start the initial library scan.
|
|
|
|
The watchfiles-based watcher has been retired in favour of Nextcloud
|
|
`webhook_listeners` (see `app.routers.nc_webhook`). NC POSTs every
|
|
file event directly to mule, so we no longer keep a long-running
|
|
inotify task. The periodic `discard_missing_photos_beat` Celery job
|
|
is still there as a safety net for deletions a webhook might miss.
|
|
"""
|
|
try:
|
|
scan_all_source_roots.delay()
|
|
logger.info("Initial scan queued successfully")
|
|
except Exception as e:
|
|
logger.error(f"Failed to start initial scan: {e}")
|