66from yuxi import config as conf
77from yuxi .utils .paths import OUTPUTS_DIR_NAME , UPLOADS_DIR_NAME , VIRTUAL_PATH_PREFIX , WORKSPACE_DIR_NAME
88
9- _SAFE_THREAD_ID_RE = re .compile (r"^[A-Za-z0-9_-]+$" )
9+ _SAFE_ID_RE = re .compile (r"^[A-Za-z0-9_-]+$" )
1010
1111
1212def get_virtual_path_prefix () -> str :
@@ -17,7 +17,7 @@ def _validate_thread_id(thread_id: str) -> str:
1717 value = str (thread_id or "" ).strip ()
1818 if not value :
1919 raise ValueError ("thread_id is required" )
20- if not _SAFE_THREAD_ID_RE .match (value ):
20+ if not _SAFE_ID_RE .match (value ):
2121 raise ValueError ("thread_id contains invalid characters" )
2222 return value
2323
@@ -27,18 +27,28 @@ def _thread_root_dir(thread_id: str) -> Path:
2727 return Path (conf .save_dir ) / "threads" / safe_thread_id / "user-data"
2828
2929
30- def _global_user_data_dir () -> Path :
31- """Return the shared host-side directory used for thread workspace files."""
32- return Path (conf .save_dir ) / "threads" / "shared"
30+ def _validate_user_id (user_id : str ) -> str :
31+ value = str (user_id or "" ).strip ()
32+ if not value :
33+ raise ValueError ("user_id is required" )
34+ if not _SAFE_ID_RE .match (value ):
35+ raise ValueError ("user_id contains invalid characters" )
36+ return value
37+
38+
39+ def _global_user_data_dir (user_id : str ) -> Path :
40+ """Return the shared host-side directory used for one user's workspace files."""
41+ safe_user_id = _validate_user_id (user_id )
42+ return Path (conf .save_dir ) / "threads" / "shared" / safe_user_id
3343
3444
3545def sandbox_user_data_dir (thread_id : str ) -> Path :
3646 return _thread_root_dir (thread_id )
3747
3848
39- def sandbox_workspace_dir (thread_id : str ) -> Path :
49+ def sandbox_workspace_dir (thread_id : str , user_id : str ) -> Path :
4050 _validate_thread_id (thread_id )
41- return _global_user_data_dir () / WORKSPACE_DIR_NAME
51+ return _global_user_data_dir (user_id ) / WORKSPACE_DIR_NAME
4252
4353
4454def sandbox_uploads_dir (thread_id : str ) -> Path :
@@ -49,14 +59,14 @@ def sandbox_outputs_dir(thread_id: str) -> Path:
4959 return _thread_root_dir (thread_id ) / OUTPUTS_DIR_NAME
5060
5161
52- def ensure_thread_dirs (thread_id : str ) -> None :
53- _global_user_data_dir ().mkdir (parents = True , exist_ok = True )
54- sandbox_workspace_dir (thread_id ).mkdir (parents = True , exist_ok = True )
62+ def ensure_thread_dirs (thread_id : str , user_id : str ) -> None :
63+ _global_user_data_dir (user_id ).mkdir (parents = True , exist_ok = True )
64+ sandbox_workspace_dir (thread_id , user_id ).mkdir (parents = True , exist_ok = True )
5565 sandbox_uploads_dir (thread_id ).mkdir (parents = True , exist_ok = True )
5666 sandbox_outputs_dir (thread_id ).mkdir (parents = True , exist_ok = True )
5767
5868
59- def _resolve_user_data_base_dir (thread_id : str , relative_path : str ) -> tuple [Path , Path ]:
69+ def _resolve_user_data_base_dir (thread_id : str , user_id : str , relative_path : str ) -> tuple [Path , Path ]:
6070 """Map a virtual user-data path to the correct host-side base directory."""
6171 parts = Path (relative_path ).parts
6272 if not parts :
@@ -65,8 +75,8 @@ def _resolve_user_data_base_dir(thread_id: str, relative_path: str) -> tuple[Pat
6575
6676 namespace = parts [0 ]
6777 if namespace == WORKSPACE_DIR_NAME :
68- # Workspace is shared across threads, so it lives outside the per-thread root.
69- base_dir = sandbox_workspace_dir (thread_id )
78+ # Workspace is shared across one user's threads, so it lives outside the per-thread root.
79+ base_dir = sandbox_workspace_dir (thread_id , user_id )
7080 target_path = base_dir .joinpath (* parts [1 :]) if len (parts ) > 1 else base_dir
7181 return base_dir .resolve (), target_path .resolve ()
7282 if namespace == UPLOADS_DIR_NAME :
@@ -82,15 +92,15 @@ def _resolve_user_data_base_dir(thread_id: str, relative_path: str) -> tuple[Pat
8292 return base_dir .resolve (), (base_dir / relative_path ).resolve ()
8393
8494
85- def resolve_virtual_path (thread_id : str , virtual_path : str ) -> Path :
95+ def resolve_virtual_path (thread_id : str , virtual_path : str , * , user_id : str ) -> Path :
8696 clean_virtual_path = "/" + str (virtual_path or "" ).strip ().lstrip ("/" )
8797 virtual_prefix = get_virtual_path_prefix ()
8898
8999 if clean_virtual_path != virtual_prefix and not clean_virtual_path .startswith (f"{ virtual_prefix } /" ):
90100 raise ValueError (f"path must start with { virtual_prefix } " )
91101
92102 relative_path = clean_virtual_path [len (virtual_prefix ) :].lstrip ("/" )
93- base_dir , target_path = _resolve_user_data_base_dir (thread_id , relative_path )
103+ base_dir , target_path = _resolve_user_data_base_dir (thread_id , user_id , relative_path )
94104
95105 try :
96106 target_path .relative_to (base_dir )
@@ -100,10 +110,10 @@ def resolve_virtual_path(thread_id: str, virtual_path: str) -> Path:
100110 return target_path
101111
102112
103- def virtual_path_for_thread_file (thread_id : str , path : str | Path ) -> str :
113+ def virtual_path_for_thread_file (thread_id : str , path : str | Path , * , user_id : str ) -> str :
104114 target_path = Path (path ).resolve ()
105115 thread_root = sandbox_user_data_dir (thread_id ).resolve ()
106- global_workspace_root = sandbox_workspace_dir (thread_id ).resolve ()
116+ global_workspace_root = sandbox_workspace_dir (thread_id , user_id ).resolve ()
107117
108118 try :
109119 relative_path = target_path .relative_to (global_workspace_root )
0 commit comments