feat: auth
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -79,6 +79,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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user