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")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
hashed = pwd_context.hash(generated_password)
|
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(
|
conn.execute(
|
||||||
sa.text(
|
sa.text(
|
||||||
"INSERT INTO users (id, username, hashed_password, role, media_path) "
|
"INSERT INTO users (id, username, hashed_password, role, media_path) "
|
||||||
@@ -91,7 +91,7 @@ def upgrade() -> None:
|
|||||||
"username": "admin",
|
"username": "admin",
|
||||||
"hashed": hashed,
|
"hashed": hashed,
|
||||||
"role": "admin",
|
"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:
|
if len(body.password) < 6:
|
||||||
raise HTTPException(status_code=400, detail="Password must be at least 6 characters")
|
raise HTTPException(status_code=400, detail="Password must be at least 6 characters")
|
||||||
|
|
||||||
# The initial admin owns the entire photo mount root. Regular users
|
# Every user — including the initial admin — gets their own subfolder
|
||||||
# (created later via admin panel) get a subdirectory under it.
|
# under the photo mount root. Nobody owns the root directory itself.
|
||||||
media_path = settings.photo_dirs
|
media_path = os.path.join(settings.photo_dirs, body.username.strip())
|
||||||
os.makedirs(media_path, exist_ok=True)
|
os.makedirs(media_path, exist_ok=True)
|
||||||
|
|
||||||
user = User(
|
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
|
hidden_folder_cache[folder_row.id] = False
|
||||||
return 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
|
# Pre-walk to compute the total file count upfront. Without this
|
||||||
# the progress bar would jump every time a new subfolder is
|
# the progress bar would jump every time a new subfolder is
|
||||||
# encountered because the running total kept growing.
|
# encountered because the running total kept growing.
|
||||||
total_files = 0
|
total_files = 0
|
||||||
for _root, _dirs, files in os.walk(folder_path):
|
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(
|
total_files += sum(
|
||||||
1 for f in files if Path(f).suffix.lower() in SUPPORTED_EXTENSIONS
|
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 = []
|
errors = []
|
||||||
|
|
||||||
for root, dirs, files in os.walk(folder_path):
|
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
|
# Get or create folder entry
|
||||||
folder = await get_or_create_folder(session, root, source_root_id, owner_user_id)
|
folder = await get_or_create_folder(session, root, source_root_id, owner_user_id)
|
||||||
progress_set(REDIS_KEY_CURRENT_FOLDER, root)
|
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
|
// Cache-bust on retry so the browser actually re-requests instead of
|
||||||
// serving the cached 404.
|
// serving the cached 404.
|
||||||
const baseUrl = photosApi.getThumbnailUrl(photo.id, 'medium')
|
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
|
// "Capture date probably wrong" — read straight from the stored
|
||||||
// `has_date_warning` flag rather than recomputing the heuristic
|
// `has_date_warning` flag rather than recomputing the heuristic
|
||||||
|
|||||||
Reference in New Issue
Block a user