Fix: Add _open() and path() methods to TenantAwareFileSystemStorage

Critical fix for Celery photo processing. The storage class now correctly
handles file reading operations by automatically adding tenant_id prefix
when opening files.

Problems fixed:
- Celery tasks could not open image files from storage
- PIL/Pillow couldn't locate files in tenant-specific directories
- temp file deletion was failing due to path validation

Changes:
- Added _open() method to add tenant_id prefix when opening files
- Added path() method to convert relative paths to full filesystem paths
- Updated delete() method to handle paths with or without tenant prefix
- All methods include security checks to prevent cross-tenant access

Testing:
- All 5 existing tests pass
- Verified photo processing task works end-to-end:
  * Reads temp image file from disk
  * Processes and creates all image versions
  * Saves processed files to tenant-specific directory
  * Cleans up temporary files correctly
- Files correctly stored in: media/tenants/{tenant_id}/products/{product_id}/{photo_id}/

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-23 20:30:52 +03:00
parent ff40a9c1f0
commit 87cba63c47
4 changed files with 114 additions and 20 deletions

View File

@@ -144,24 +144,30 @@ class TenantAwareFileSystemStorage(FileSystemStorage):
Удалить файл, убедившись что он принадлежит текущему тенанту.
Args:
name (str): Путь к файлу
name (str): Путь к файлу (может быть БЕЗ tenant_id)
"""
# Получаем tenant_id для проверки
tenant_id = self._get_tenant_id()
# Проверяем что файл принадлежит текущему тенанту
if not name.startswith(f"tenants/{tenant_id}/"):
logger.warning(
f"[Storage] Security: Attempted to delete file from different tenant! "
f"Current tenant: {tenant_id}, file: {name}"
)
raise RuntimeError(
f"Cannot delete file - it belongs to a different tenant. "
f"Current tenant: {tenant_id}"
)
# Если путь уже содержит tenants/, проверяем принадлежность тенанту
if name.startswith("tenants/"):
if not name.startswith(f"tenants/{tenant_id}/"):
logger.warning(
f"[Storage] Security: Attempted to delete file from different tenant! "
f"Current tenant: {tenant_id}, file: {name}"
)
raise RuntimeError(
f"Cannot delete file - it belongs to a different tenant. "
f"Current tenant: {tenant_id}"
)
# Если путь уже содержит tenants/, удаляем его как есть
logger.debug(f"[Storage] delete: {name} (already has tenant prefix)")
return super().delete(name)
logger.debug(f"[Storage] delete: {name}")
return super().delete(name)
# Иначе добавляем tenant_id перед удалением
tenant_aware_name = self._get_tenant_path(name)
logger.debug(f"[Storage] delete: {name}{tenant_aware_name}")
return super().delete(tenant_aware_name)
def exists(self, name):
"""
@@ -220,3 +226,76 @@ class TenantAwareFileSystemStorage(FileSystemStorage):
# Иначе добавляем tenant_id
tenant_aware_name = self._get_tenant_path(name)
return super().url(tenant_aware_name)
def _open(self, name, mode='rb'):
"""
Открыть файл, добавив tenant_id в путь если необходимо.
Это критически важно для Celery задач, которые читают файлы из БД.
Когда ImageProcessor вызывает Image.open(photo_obj.image), Django вызывает
это метод. БД содержит путь БЕЗ tenant_id (например, 'products/temp/image.jpg'),
но файл находится на диске с tenant_id (tenants/{tenant_id}/products/temp/image.jpg).
Args:
name (str): Путь к файлу (может быть БЕЗ tenant_id)
mode (str): Режим открытия файла
Returns:
File-like object
"""
# Получаем tenant_id
tenant_id = self._get_tenant_id()
# Если путь уже содержит tenants/, проверяем принадлежность тенанту
if name.startswith("tenants/"):
if not name.startswith(f"tenants/{tenant_id}/"):
logger.warning(
f"[Storage] Security: Attempted to open file from different tenant! "
f"Current tenant: {tenant_id}, file: {name}"
)
raise RuntimeError(
f"Cannot open file - it belongs to a different tenant. "
f"Current tenant: {tenant_id}"
)
# Если путь уже содержит tenants/, используем его как есть
logger.debug(f"[Storage] _open: {name} (already has tenant prefix)")
return super()._open(name, mode)
# Иначе добавляем tenant_id перед открытием
tenant_aware_name = self._get_tenant_path(name)
logger.debug(f"[Storage] _open: {name}{tenant_aware_name}")
return super()._open(tenant_aware_name, mode)
def path(self, name):
"""
Получить полный системный путь к файлу, добавив tenant_id если необходимо.
Используется для конвертации 'products/temp/image.jpg' в '/media/tenants/papa/products/temp/image.jpg'
Args:
name (str): Путь к файлу (может быть БЕЗ tenant_id)
Returns:
str: Полный системный путь к файлу
"""
# Получаем tenant_id
tenant_id = self._get_tenant_id()
# Если путь уже содержит tenants/, проверяем принадлежность тенанту
if name.startswith("tenants/"):
if not name.startswith(f"tenants/{tenant_id}/"):
logger.warning(
f"[Storage] Security: Attempted to get path for file from different tenant! "
f"Current tenant: {tenant_id}, file: {name}"
)
raise RuntimeError(
f"Cannot get path for file - it belongs to a different tenant. "
f"Current tenant: {tenant_id}"
)
# Если путь уже содержит tenants/, используем его как есть
logger.debug(f"[Storage] path: {name} (already has tenant prefix)")
return super().path(name)
# Иначе добавляем tenant_id перед получением пути
tenant_aware_name = self._get_tenant_path(name)
logger.debug(f"[Storage] path: {name}{tenant_aware_name}")
return super().path(tenant_aware_name)