diff --git a/backend/alembic/versions/0009_users_and_auth.py b/backend/alembic/versions/0009_users_and_auth.py index 1948233..e35a7f3 100644 --- a/backend/alembic/versions/0009_users_and_auth.py +++ b/backend/alembic/versions/0009_users_and_auth.py @@ -80,7 +80,7 @@ def upgrade() -> None: pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") hashed = pwd_context.hash(generated_password) - # The default admin's media_path is the legacy /photos root + # Every user gets a subfolder — including the migrated admin. conn.execute( sa.text( "INSERT INTO users (id, username, hashed_password, role, media_path) " @@ -91,7 +91,7 @@ def upgrade() -> None: "username": "admin", "hashed": hashed, "role": "admin", - "media_path": "/photos", + "media_path": "/photos/admin", }, ) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 91ac972..eed115f 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -161,9 +161,9 @@ async def setup(body: SetupRequest, db: AsyncSession = Depends(get_db)): if len(body.password) < 6: raise HTTPException(status_code=400, detail="Password must be at least 6 characters") - # The initial admin owns the entire photo mount root. Regular users - # (created later via admin panel) get a subdirectory under it. - media_path = settings.photo_dirs + # Every user — including the initial admin — gets their own subfolder + # under the photo mount root. Nobody owns the root directory itself. + media_path = os.path.join(settings.photo_dirs, body.username.strip()) os.makedirs(media_path, exist_ok=True) user = User( diff --git a/backend/app/tasks/scan.py b/backend/app/tasks/scan.py index 4a38317..8fcbb55 100644 --- a/backend/app/tasks/scan.py +++ b/backend/app/tasks/scan.py @@ -164,28 +164,11 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta hidden_folder_cache[folder_row.id] = False return False - # Build a set of paths owned by OTHER users' source roots so - # the admin's scan of /photos doesn't descend into /photos/bob. - other_root_paths: set[str] = set() - if owner_user_id: - other_roots = (await session.execute( - select(SourceRoot.path) - .where(SourceRoot.is_active == True) # noqa: E712 - .where(SourceRoot.user_id != owner_user_id) - )).scalars().all() - other_root_paths = {os.path.normpath(p) for p in other_roots} - - def _should_skip_dir(dirpath: str) -> bool: - """True if dirpath is another user's source root.""" - return os.path.normpath(dirpath) in other_root_paths - # Pre-walk to compute the total file count upfront. Without this # the progress bar would jump every time a new subfolder is # encountered because the running total kept growing. total_files = 0 for _root, _dirs, files in os.walk(folder_path): - # Prune other users' directories from count - _dirs[:] = [d for d in _dirs if not _should_skip_dir(os.path.join(_root, d))] total_files += sum( 1 for f in files if Path(f).suffix.lower() in SUPPORTED_EXTENSIONS ) @@ -196,9 +179,6 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta errors = [] for root, dirs, files in os.walk(folder_path): - # Skip directories that belong to other users' source roots - dirs[:] = [d for d in dirs if not _should_skip_dir(os.path.join(root, d))] - # Get or create folder entry folder = await get_or_create_folder(session, root, source_root_id, owner_user_id) progress_set(REDIS_KEY_CURRENT_FOLDER, root) diff --git a/frontend/src/components/timeline/PhotoThumbnail.tsx b/frontend/src/components/timeline/PhotoThumbnail.tsx index f9d6093..c387fdd 100644 --- a/frontend/src/components/timeline/PhotoThumbnail.tsx +++ b/frontend/src/components/timeline/PhotoThumbnail.tsx @@ -100,7 +100,8 @@ export function PhotoThumbnail({ // Cache-bust on retry so the browser actually re-requests instead of // serving the cached 404. const baseUrl = photosApi.getThumbnailUrl(photo.id, 'medium') - const thumbnailUrl = retryCount > 0 ? `${baseUrl}?retry=${retryCount}` : baseUrl + const sep = baseUrl.includes('?') ? '&' : '?' + const thumbnailUrl = retryCount > 0 ? `${baseUrl}${sep}retry=${retryCount}` : baseUrl // "Capture date probably wrong" — read straight from the stored // `has_date_warning` flag rather than recomputing the heuristic