From b59d0e4f2c44b867ffcc9c4a69fc7400dd009be6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 18:03:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EA=B3=B5=ED=86=B5):=20storage=EA=B0=80=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=EC=9D=BC=20=EB=95=8C=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B2=BD=EB=A1=9C=20=EA=B8=B0=EC=A4=80=EC=9D=B4=20?= =?UTF-8?q?=EC=96=B4=EA=B8=8B=EB=82=98=20=ED=84=B0=EC=A7=80=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=EC=9D=84=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `storage/`를 심볼릭 링크·정션으로 두면(워크트리를 나눠 쓰면 실제로 그렇다) 프로젝트 루트는 링크 경로 그대로인데 저장 엔진(resolve_upload_destination· resolve_chunk_session_dir)은 Path.resolve()로 링크를 따라가 실경로를 만든다. 그래서 chunk_path.relative_to(project_root)가 ValueError로 터졌다. '...본폴더\storage\1\3\{id}\B03_FileInput\chunks_temp\...\00000000.chunk' is not in the subpath of '...워크트리\storage\1\3\{id}' 프로젝트 루트와 임시 보관함 루트를 realpath로 돌려주어 기준을 하나로 맞춘다. 경로 검증(상대경로·.. 금지·루트 이탈)은 그대로다. 검증: tmp/tests/test_storage_symlink_root.py 추가 — 링크된 storage에서 반환 경로가 resolve() 결과와 같고 relative_to가 성립하는지. 전체 9 passed. --- common_util/common_util_storage.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/common_util/common_util_storage.py b/common_util/common_util_storage.py index b367b809..4afb11d3 100644 --- a/common_util/common_util_storage.py +++ b/common_util/common_util_storage.py @@ -60,8 +60,9 @@ def resolve_temp_batch_path(user_id: int, batch_id: str, *, create: bool = True) if not batch_id or any(sep in batch_id for sep in ("/", "\\", "..")): raise ValueError("임시 보관함 묶음 식별자가 올바르지 않습니다.") - temp_root = os.path.abspath(os.path.join(STORAGE_BASE_DIR, TEMP_UPLOAD_DIR_NAME)) - path = os.path.abspath(os.path.join(temp_root, str(user_id), batch_id)) + # 실경로로 맞춘다 — 저장 엔진이 `Path.resolve()`를 쓰므로 기준이 같아야 한다. + temp_root = os.path.realpath(os.path.join(STORAGE_BASE_DIR, TEMP_UPLOAD_DIR_NAME)) + path = os.path.realpath(os.path.join(temp_root, str(user_id), batch_id)) if os.path.commonpath((temp_root, path)) != temp_root or path == temp_root: raise ValueError("임시 보관함 경로가 보관함 루트를 벗어났습니다.") if create: @@ -70,8 +71,8 @@ def resolve_temp_batch_path(user_id: int, batch_id: str, *, create: bool = True) def temp_upload_root() -> str: - """임시 보관함 루트(`storage/tmp`) 절대 경로.""" - return os.path.abspath(os.path.join(STORAGE_BASE_DIR, TEMP_UPLOAD_DIR_NAME)) + """임시 보관함 루트(`storage/tmp`) 실경로.""" + return os.path.realpath(os.path.join(STORAGE_BASE_DIR, TEMP_UPLOAD_DIR_NAME)) def resolve_project_root_for_delete(relative_path: str, project_id: str) -> str: @@ -100,15 +101,20 @@ def resolve_project_root_for_delete(relative_path: str, project_id: str) -> str: def resolve_stored_project_path(relative_path: str) -> str: - """DB의 storage 기준 상대 경로를 검증해 실제 프로젝트 경로로 변환한다.""" + """DB의 storage 기준 상대 경로를 검증해 실제 프로젝트 경로로 변환한다. + + 실경로(`realpath`)로 돌려준다. `storage/`가 심볼릭 링크·정션일 수 있고(워크트리를 + 나눠 쓰면 실제로 그렇다), 저장 엔진 쪽은 `Path.resolve()`로 링크를 따라간다. 두 + 경로의 기준이 다르면 `chunk_path.relative_to(project_root)`가 터진다. + """ normalized = PurePosixPath(relative_path.replace("\\", "/")) if normalized.is_absolute() or ".." in normalized.parts: raise ValueError("프로젝트 저장 경로는 안전한 상대 경로여야 합니다.") if not normalized.parts or normalized.parts[0] != "storage": raise ValueError("프로젝트 저장 경로는 storage/로 시작해야 합니다.") - storage_root = os.path.abspath(STORAGE_BASE_DIR) - path = os.path.abspath(os.path.join(storage_root, *normalized.parts[1:])) + storage_root = os.path.realpath(STORAGE_BASE_DIR) + path = os.path.realpath(os.path.join(storage_root, *normalized.parts[1:])) if os.path.commonpath((storage_root, path)) != storage_root or path == storage_root: raise ValueError("프로젝트 저장 경로가 저장소 루트를 벗어났습니다.") os.makedirs(path, exist_ok=True)