merge: integrate feat/auth into develop

This commit is contained in:
Winnie
2026-08-03 17:44:00 +08:00
38 changed files with 2590 additions and 796 deletions
+2
View File
@@ -10,6 +10,8 @@ dependencies = [
"boto3>=1.34,<2",
"fastapi==0.116.1",
"pydantic-settings>=2.14.2",
"passlib==1.7.4",
"bcrypt>=4.0,<4.1",
]
[build-system]
+16
View File
@@ -83,6 +83,22 @@ class Settings(BaseSettings):
default="run-logs",
description="Bucket for schedule run logs.",
)
rustfs_trash_bucket: str = Field(
default="trash",
description=(
"Bucket for soft-deleted objects. The source bucket key is "
"preserved as a prefix so a restore is a same-key move. "
"Trash is reaped on a schedule out of band."
),
)
rustfs_trash_retention_days: int = Field(
default=30,
description=(
"How long a trashed object is retained before reaping. "
"Tracked in the database (StorageObjects.deleted_at) so the "
"reaper can run as a single SQL sweep."
),
)
# ── local FS roots ────────────────────────────────────────────
workspace_root: str = Field(
+9
View File
@@ -103,6 +103,15 @@ class StorageObjects(Base):
TINYINT(1), nullable=False, server_default=text("0")
)
deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3))
trash_key: Mapped[Optional[str]] = mapped_column(
String(1100),
comment=(
"Path inside the trash bucket where the soft-deleted bytes "
"live. Format: '{source_bucket}/{object_key}' so a restore "
"is a same-key copy back to the source bucket. NULL while "
"the row is still available."
),
)
class DataResources(Base):
+21
View File
@@ -16,6 +16,18 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from common.scheduler.trigger import (
SYSTEM_CRON_USER_ID,
DagTooLarge,
InvalidDag,
InvalidNodeArguments,
ScheduleNotFound,
TriggerError,
create_scheduled_run,
normalize_idempotency_key,
parse_node_arguments,
)
if TYPE_CHECKING: # pragma: no cover - typing only
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
@@ -53,7 +65,16 @@ def build_sqlalchemy_jobstore(
__all__ = [
"DagTooLarge",
"InvalidDag",
"InvalidNodeArguments",
"JOBSTORE_TABLE",
"SYSTEM_CRON_USER_ID",
"ScheduleNotFound",
"TriggerError",
"build_sqlalchemy_jobstore",
"create_scheduled_run",
"normalize_idempotency_key",
"parse_node_arguments",
"to_sync_database_url",
]
+47
View File
@@ -156,3 +156,50 @@ class RustFSObjectStore:
def delete(self, *, bucket_name: str, object_key: str) -> None:
self.internal.delete_object(Bucket=bucket_name, Key=object_key)
def copy(
self,
*,
source_bucket: str,
source_key: str,
dest_bucket: str,
dest_key: str,
) -> None:
"""Server-side copy ``source_bucket/source_key`` → ``dest_bucket/dest_key``.
``CopySource`` is a single header string of the form
``/{bucket}/{key}`` — must NOT be URL-encoded or quoted.
"""
self.internal.copy_object(
Bucket=dest_bucket,
Key=dest_key,
CopySource={"Bucket": source_bucket, "Key": source_key},
)
def move_to_trash(
self,
*,
source_bucket: str,
source_key: str,
trash_bucket: str,
trash_key: str,
) -> None:
"""Copy an object into the trash bucket and delete the source.
The copy is a server-side operation in RustFS (no data flows
through the client). The source delete is best-effort: if it
fails after the copy succeeds the trash holds the only copy of
the bytes, which is exactly the point — the caller can retry.
"""
self.copy(
source_bucket=source_bucket,
source_key=source_key,
dest_bucket=trash_bucket,
dest_key=trash_key,
)
try:
self.delete(bucket_name=source_bucket, object_key=source_key)
except ClientError:
# Source was already gone, or transient delete failure —
# the trash copy is what matters; caller logs and moves on.
pass