fix: all users get subfolders, nobody owns the mount root
Every user — including the initial admin — now gets their own
subdirectory under PHOTO_DIRS (e.g. /photos/admin, /photos/bob).
No one's source root points to the mount root itself, eliminating
cross-user photo overlap entirely.
- Setup endpoint: admin gets /photos/{username} like everyone else
- Migration: default admin media_path set to /photos/admin
- Remove scan directory pruning (no longer needed)
- Fix thumbnail retry URL: use & separator when token query param
already present (was producing ?token=...?retry=N)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user