update: update storage struct
This commit is contained in:
@@ -395,9 +395,14 @@ async def create_script_record(
|
|||||||
# the legacy flow no longer applies. We still do a Scripts-only
|
# the legacy flow no longer applies. We still do a Scripts-only
|
||||||
# conflict check on the user-supplied name so two scripts cannot
|
# conflict check on the user-supplied name so two scripts cannot
|
||||||
# claim the same display name within the same workspace.
|
# claim the same display name within the same workspace.
|
||||||
|
user_id = context.user.user_id
|
||||||
script_id = new_ulid()
|
script_id = new_ulid()
|
||||||
jupyter_name = _jupyter_path(script_type, script_id)
|
jupyter_basename = _jupyter_path(script_type, script_id)
|
||||||
nested_jupyter_name = f"{parent_ulid}/{jupyter_name}" if parent_ulid else jupyter_name
|
# Jupyter path inside workspace root: {user_id}/{parent_or_self}/{basename}
|
||||||
|
if parent_ulid:
|
||||||
|
jupyter_path = f"{user_id}/{parent_ulid}/{jupyter_basename}"
|
||||||
|
else:
|
||||||
|
jupyter_path = f"{user_id}/{jupyter_basename}"
|
||||||
name_clash = await session.scalar(
|
name_clash = await session.scalar(
|
||||||
select(Scripts.script_id).where(
|
select(Scripts.script_id).where(
|
||||||
Scripts.workspace_id == context.workspace.workspace_id,
|
Scripts.workspace_id == context.workspace.workspace_id,
|
||||||
@@ -421,7 +426,7 @@ async def create_script_record(
|
|||||||
size_bytes = len(content)
|
size_bytes = len(content)
|
||||||
if parent_ulid:
|
if parent_ulid:
|
||||||
try:
|
try:
|
||||||
await runtime_client.ensure_directory(workspace_id, parent_ulid)
|
await runtime_client.ensure_directory(workspace_id, f"{user_id}/{parent_ulid}")
|
||||||
except RuntimeClientError as exc:
|
except RuntimeClientError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
@@ -434,15 +439,15 @@ async def create_script_record(
|
|||||||
logger.debug(notebook)
|
logger.debug(notebook)
|
||||||
jupyter_resp = await runtime_client.create_notebook(
|
jupyter_resp = await runtime_client.create_notebook(
|
||||||
workspace_id,
|
workspace_id,
|
||||||
name=nested_jupyter_name,
|
name=jupyter_path,
|
||||||
cells=notebook.get("cells"),
|
cells=notebook.get("cells"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
jupyter_resp = await runtime_client.upload_file(
|
jupyter_resp = await runtime_client.upload_file(
|
||||||
workspace_id,
|
workspace_id,
|
||||||
name=nested_jupyter_name,
|
name=jupyter_path,
|
||||||
content=content.decode("utf-8"),
|
content=content.decode("utf-8"),
|
||||||
content_type=(mimetypes.guess_type(jupyter_name)[0] or "text/plain"),
|
content_type=(mimetypes.guess_type(jupyter_basename)[0] or "text/plain"),
|
||||||
)
|
)
|
||||||
logger.debug(jupyter_resp)
|
logger.debug(jupyter_resp)
|
||||||
except RuntimeClientError as exc:
|
except RuntimeClientError as exc:
|
||||||
@@ -458,12 +463,12 @@ async def create_script_record(
|
|||||||
# file is queryable as a workspace file from the user's POV; the
|
# file is queryable as a workspace file from the user's POV; the
|
||||||
# storage_uri points at where the replicated bytes will land.
|
# storage_uri points at where the replicated bytes will land.
|
||||||
object_id = new_ulid()
|
object_id = new_ulid()
|
||||||
object_key = f"{workspace_id}/{nested_jupyter_name}"
|
object_key = f"{workspace_id}/{jupyter_path}"
|
||||||
bucket_name = settings.s3_workspace_bucket
|
bucket_name = settings.s3_workspace_bucket
|
||||||
relative_path = user_relative_path(
|
relative_path = user_relative_path(
|
||||||
context, f"{parent}/{jupyter_name}" if parent else jupyter_name
|
context, f"{parent}/{jupyter_basename}" if parent else jupyter_basename
|
||||||
)
|
)
|
||||||
mime_type = mimetypes.guess_type(jupyter_name)[0]
|
mime_type = mimetypes.guess_type(jupyter_basename)[0]
|
||||||
storage_object = StorageObjects(
|
storage_object = StorageObjects(
|
||||||
storage_object_id=object_id,
|
storage_object_id=object_id,
|
||||||
workspace_id=context.workspace.workspace_id,
|
workspace_id=context.workspace.workspace_id,
|
||||||
@@ -476,7 +481,7 @@ async def create_script_record(
|
|||||||
object_key_hash=hashlib.sha256(object_key.encode("utf-8")).digest(),
|
object_key_hash=hashlib.sha256(object_key.encode("utf-8")).digest(),
|
||||||
storage_uri=build_storage_uri(bucket_name, object_key),
|
storage_uri=build_storage_uri(bucket_name, object_key),
|
||||||
file_name=name,
|
file_name=name,
|
||||||
file_extension=PurePosixPath(jupyter_name).suffix.lower() or None,
|
file_extension=PurePosixPath(jupyter_basename).suffix.lower() or None,
|
||||||
mime_type=mime_type,
|
mime_type=mime_type,
|
||||||
size_bytes=size_bytes,
|
size_bytes=size_bytes,
|
||||||
content_hash=content_hash,
|
content_hash=content_hash,
|
||||||
@@ -507,13 +512,13 @@ async def create_script_record(
|
|||||||
# Best-effort compensating cleanup: remove the Jupyter file we
|
# Best-effort compensating cleanup: remove the Jupyter file we
|
||||||
# just created so a failed flush does not leave an orphan on disk.
|
# just created so a failed flush does not leave an orphan on disk.
|
||||||
try:
|
try:
|
||||||
await runtime_client.delete_file(workspace_id, name=nested_jupyter_name)
|
await runtime_client.delete_file(workspace_id, name=jupyter_path)
|
||||||
except RuntimeClientError as cleanup_exc:
|
except RuntimeClientError as cleanup_exc:
|
||||||
if cleanup_exc.status_code == 404:
|
if cleanup_exc.status_code == 404:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"failed to clean up Jupyter file {nested_jupyter_name} "
|
f"failed to clean up Jupyter file {jupyter_path} "
|
||||||
f"after DB flush error: {cleanup_exc.status_code} {cleanup_exc.detail}"
|
f"after DB flush error: {cleanup_exc.status_code} {cleanup_exc.detail}"
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
@@ -813,13 +818,17 @@ async def create_workspace_directory(
|
|||||||
# the on-disk directory name match. Create the directory in Jupyter
|
# the on-disk directory name match. Create the directory in Jupyter
|
||||||
# before persisting the DB row; if persistence fails we clean up.
|
# before persisting the DB row; if persistence fails we clean up.
|
||||||
dir_id = new_ulid()
|
dir_id = new_ulid()
|
||||||
jupyter_path = f"{parent_ulid}/{dir_id}" if parent_ulid else dir_id
|
user_id = context.user.user_id
|
||||||
|
if parent_ulid:
|
||||||
|
jupyter_path = f"{user_id}/{parent_ulid}/{dir_id}"
|
||||||
|
else:
|
||||||
|
jupyter_path = f"{user_id}/{dir_id}"
|
||||||
runtime_client = request.app.state.runtime_client
|
runtime_client = request.app.state.runtime_client
|
||||||
workspace_id = context.workspace.workspace_id
|
workspace_id = context.workspace.workspace_id
|
||||||
|
|
||||||
if parent_ulid:
|
if parent_ulid:
|
||||||
try:
|
try:
|
||||||
await runtime_client.ensure_directory(workspace_id, parent_ulid)
|
await runtime_client.ensure_directory(workspace_id, f"{user_id}/{parent_ulid}")
|
||||||
except RuntimeClientError as exc:
|
except RuntimeClientError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
@@ -962,7 +971,7 @@ async def delete_workspace_directory(
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
await runtime_client.delete_directory(
|
await runtime_client.delete_directory(
|
||||||
workspace_id, name=descendant.storage_object_id
|
workspace_id, name=f"{descendant.owner_user_id}/{descendant.storage_object_id}"
|
||||||
)
|
)
|
||||||
except RuntimeClientError as exc:
|
except RuntimeClientError as exc:
|
||||||
if exc.status_code != 404:
|
if exc.status_code != 404:
|
||||||
@@ -976,7 +985,7 @@ async def delete_workspace_directory(
|
|||||||
descendant.deleted_at = datetime.now(UTC).replace(tzinfo=None)
|
descendant.deleted_at = datetime.now(UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await runtime_client.delete_directory(workspace_id, name=target_ulid)
|
await runtime_client.delete_directory(workspace_id, name=f"{context.user.user_id}/{target_ulid}")
|
||||||
except RuntimeClientError as exc:
|
except RuntimeClientError as exc:
|
||||||
if exc.status_code != 404:
|
if exc.status_code != 404:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -1044,26 +1053,25 @@ async def get_script_content(
|
|||||||
适用于被锁定的脚本,非所有者只能查看内容,不能编辑。
|
适用于被锁定的脚本,非所有者只能查看内容,不能编辑。
|
||||||
该接口不检查锁状态,调用方需自行判断权限。
|
该接口不检查锁状态,调用方需自行判断权限。
|
||||||
"""
|
"""
|
||||||
script = await session.scalar(
|
script, storage_object = await get_script_row(
|
||||||
select(Scripts)
|
script_id,
|
||||||
.where(
|
context,
|
||||||
Scripts.script_id == script_id,
|
session,
|
||||||
Scripts.workspace_id == context.workspace.workspace_id,
|
allow_missing_storage_object=True,
|
||||||
Scripts.status == "active",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if script is None:
|
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "script not found")
|
|
||||||
|
|
||||||
# 获取 Jupyter 路径
|
# 获取 Jupyter 路径
|
||||||
jupyter_name = _jupyter_path(script.script_type, script.script_id)
|
workspace_id = context.workspace.workspace_id
|
||||||
|
jupyter_path = _derive_jupyter_path(
|
||||||
|
storage_object, workspace_id, script.script_type, script.script_id
|
||||||
|
)
|
||||||
|
|
||||||
# 从 Jupyter 读取内容
|
# 从 Jupyter 读取内容
|
||||||
runtime_client = request.app.state.runtime_client
|
runtime_client = request.app.state.runtime_client
|
||||||
try:
|
try:
|
||||||
content_data = await runtime_client.get_file(
|
content_data = await runtime_client.get_file(
|
||||||
context.workspace.workspace_id,
|
workspace_id,
|
||||||
name=jupyter_name,
|
name=jupyter_path,
|
||||||
)
|
)
|
||||||
except RuntimeClientError as exc:
|
except RuntimeClientError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ async def create_upload_record(
|
|||||||
# Jupyter selects its editor from this suffix, so an extensionless
|
# Jupyter selects its editor from this suffix, so an extensionless
|
||||||
# object would make notebooks look like generic JSON/text files.
|
# object would make notebooks look like generic JSON/text files.
|
||||||
file_extension = PurePosixPath(_safe_file_name(payload.file_name)).suffix.lower()
|
file_extension = PurePosixPath(_safe_file_name(payload.file_name)).suffix.lower()
|
||||||
object_key = f"{payload.workspace_id}/{new_ulid()}{file_extension}"
|
object_key = f"{payload.workspace_id}/{payload.user_id}/{new_ulid()}{file_extension}"
|
||||||
upload = UploadSessions(
|
upload = UploadSessions(
|
||||||
upload_id=new_ulid(),
|
upload_id=new_ulid(),
|
||||||
workspace_id=payload.workspace_id,
|
workspace_id=payload.workspace_id,
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ async def create_upload_record(
|
|||||||
# Jupyter selects its editor from this suffix, so an extensionless
|
# Jupyter selects its editor from this suffix, so an extensionless
|
||||||
# object would make notebooks look like generic JSON/text files.
|
# object would make notebooks look like generic JSON/text files.
|
||||||
file_extension = PurePosixPath(safe_file_name(payload.file_name)).suffix.lower()
|
file_extension = PurePosixPath(safe_file_name(payload.file_name)).suffix.lower()
|
||||||
object_key = f"{payload.workspace_id}/{upload_id}{file_extension}"
|
object_key = f"{payload.workspace_id}/{payload.user_id}/{upload_id}{file_extension}"
|
||||||
upload = UploadSessions(
|
upload = UploadSessions(
|
||||||
upload_id=upload_id,
|
upload_id=upload_id,
|
||||||
workspace_id=payload.workspace_id,
|
workspace_id=payload.workspace_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user