diff --git a/ROADMAP.md b/ROADMAP.md index af891884..0cb34d04 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -206,7 +206,7 @@ Order = user-visible value first; each ships with backend + UI + tests. - Fix upstream bug backlog by impact: ~~#3574 OOM~~ (done — paged tree walk + batched delete), ~~#3118/#3005 WebDAV large-file~~ (done — Content-Range assembly into one session; non-local policies get honest 501; single-PUT giant-file 500s are proxy/client timeouts, not fixable server-side), ~~#3375 SMTP auth discovery~~ (done — `smtp_auth` setting) - #3454 (PG FK on upload) is **Pro-only** — `audit_logs` doesn't exist in this codebase. When B.5 adds our own audit log: insert the audit row in the same tx *after* the file row, never before. - [ ] #199 (upstream #3584) — markdown editor lag: profile the MDX editor path; likely re-render-per-keystroke, evaluate debounce/virtualization or lighter editor before swapping libraries -- [ ] #198 (upstream #3581) — "import files" task shows source storage policy "unknown": check task props → policy name resolution in admin import path (Pro report, likely same code path in CE) +- [x] #198 (upstream #3581) — "import files" task shows source storage policy "unknown": `ImportTaskState.PolicyName` baked at creation (resolved best-effort via `StoragePolicyClient`), summary emits `dst_policy_name` so admin views of other users' tasks work without a policy lookup; `policyOptionCache` retyped to `StoragePolicyBrief[]` and populated from `getAllowedPolicies()` at session init as fallback for legacy tasks - [ ] #200 (upstream #3586) — Pro crash on SIGHUP; log shows a clean signal-driven shutdown, no stack trace — watch for a CE repro, likely not actionable yet - [x] `desloppify` pass — 73 review items dispositioned (46 fixed, 27 honestly skipped), strict score 77.1 (was 18.9); scorecard lives in README. `security-reviewer` pass done incrementally per batch (OAuth secrets, SSRF, process exec, path safety) - [x] Tag management page (upstream #2962) — owner-scoped `tag:` metadata stats/rename/recolor/delete in `inventory.FileClient`, `GET/PATCH/DELETE /file/tag` routes, Settings → Tags tab with merge-on-rename semantics diff --git a/frontend/src/api/workflow.ts b/frontend/src/api/workflow.ts index e0b7004e..e090c2c4 100644 --- a/frontend/src/api/workflow.ts +++ b/frontend/src/api/workflow.ts @@ -37,6 +37,7 @@ export interface TaskSummary { dst?: string; src_multiple?: string[]; dst_policy_id?: string; + dst_policy_name?: string; failed?: number; total?: number; download?: DownloadTaskStatus; diff --git a/frontend/src/component/Pages/Tasks/TaskSummaryTitle.tsx b/frontend/src/component/Pages/Tasks/TaskSummaryTitle.tsx index cf026512..56652b91 100644 --- a/frontend/src/component/Pages/Tasks/TaskSummaryTitle.tsx +++ b/frontend/src/component/Pages/Tasks/TaskSummaryTitle.tsx @@ -93,9 +93,11 @@ const TaskSummaryTitle = ({ type, summary, isInDashboard = false }: TaskSummaryT p.id == summary?.props.dst_policy_id)?.name ?? "Unknown" - : "", + policy: + summary?.props.dst_policy_name || + (policyOption + ? policyOption.find((p) => p.id == summary?.props.dst_policy_id)?.name ?? "Unknown" + : "Unknown"), }} components={[ { state.remoteDownloadDialogOpen = false; }, - setPolicyOptionCache: (state, action: PayloadAction) => { + setPolicyOptionCache: (state, action: PayloadAction) => { state.policyOptionCache = action.payload; }, resetDialogs: (state) => { diff --git a/frontend/src/redux/thunks/session.ts b/frontend/src/redux/thunks/session.ts index e0c75969..59813499 100644 --- a/frontend/src/redux/thunks/session.ts +++ b/frontend/src/redux/thunks/session.ts @@ -1,6 +1,6 @@ import i18next from "i18next"; import { enqueueSnackbar } from "notistack"; -import { getUserInfo, sendSignout } from "../../api/api.ts"; +import { getAllowedPolicies, getUserInfo, sendSignout } from "../../api/api.ts"; import { LoginResponse, User } from "../../api/user.ts"; import { DefaultCloseAction } from "../../component/Common/Snackbar/snackbar.tsx"; import { router } from "../../router"; @@ -43,7 +43,12 @@ export function setTargetSession(session: LoginResponse): AppThunk { dispatch(setDrawerWidth(SessionManager.getWithFallback(UserSettings.DrawerWidth))); dispatch(setDarkMode(SessionManager.get(UserSettings.PreferredDarkMode))); // TODO: clear fm cache - dispatch(setPolicyOptionCache()); + // Populate the policy id→name cache used by task summaries (e.g. + // import tasks show the source policy). Fire-and-forget so a slow or + // failing request never blocks login. + dispatch(getAllowedPolicies()) + .then((policies) => dispatch(setPolicyOptionCache(policies))) + .catch(() => {}); dispatch(clearSessionCache({ index: 0, value: undefined })); refreshTimeZone(); }; diff --git a/pkg/filemanager/workflows/import.go b/pkg/filemanager/workflows/import.go index 39e9183c..e5f4828b 100644 --- a/pkg/filemanager/workflows/import.go +++ b/pkg/filemanager/workflows/import.go @@ -32,6 +32,7 @@ type ( } ImportTaskState struct { PolicyID int `json:"policy_id"` + PolicyName string `json:"policy_name,omitempty"` Src string `json:"src"` Recursive bool `json:"is_recursive"` Dst string `json:"dst"` @@ -55,12 +56,13 @@ func init() { queue.RegisterResumableTaskFactory(queue.ImportTaskType, NewImportTaskFromModel) } -func NewImportTask(ctx context.Context, u *ent.User, src string, recursive bool, dst string, policyID int) (queue.Task, error) { +func NewImportTask(ctx context.Context, u *ent.User, src string, recursive bool, dst string, policyID int, policyName string) (queue.Task, error) { state := &ImportTaskState{ - Src: src, - Recursive: recursive, - Dst: dst, - PolicyID: policyID, + Src: src, + Recursive: recursive, + Dst: dst, + PolicyID: policyID, + PolicyName: policyName, } stateBytes, err := json.Marshal(state) if err != nil { @@ -235,6 +237,7 @@ func (m *ImportTask) Summarize(hasher hashid.Encoder) *queue.Summary { SummaryKeySrcStr: m.state.Src, SummaryKeyFailed: m.state.Failed, SummaryKeySrcDstPolicyID: hashid.EncodePolicyID(hasher, m.state.PolicyID), + SummaryKeyDstPolicyName: m.state.PolicyName, }, } } diff --git a/pkg/filemanager/workflows/import_test.go b/pkg/filemanager/workflows/import_test.go new file mode 100644 index 00000000..50bfb3b2 --- /dev/null +++ b/pkg/filemanager/workflows/import_test.go @@ -0,0 +1,60 @@ +package workflows + +import ( + "context" + "testing" + + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/pkg/hashid" + "github.com/cloudreve/Cloudreve/v4/pkg/queue" + "github.com/stretchr/testify/require" +) + +func TestImportTaskSummarizeIncludesPolicyName(t *testing.T) { + hasher, err := hashid.New("test-salt") + require.NoError(t, err) + + owner := &ent.User{ID: 1} + task, err := NewImportTask(context.Background(), owner, "/src/path", true, "cloudreve://my/dst", 7, "Local Storage") + require.NoError(t, err) + + summary := task.Summarize(hasher) + require.NotNil(t, summary) + require.Equal(t, "Local Storage", summary.Props[SummaryKeyDstPolicyName]) + require.Equal(t, hashid.EncodePolicyID(hasher, 7), summary.Props[SummaryKeySrcDstPolicyID]) + require.Equal(t, "/src/path", summary.Props[SummaryKeySrcStr]) + require.Equal(t, "cloudreve://my/dst", summary.Props[SummaryKeyDst]) +} + +func TestImportTaskSummarizeLegacyState(t *testing.T) { + hasher, err := hashid.New("test-salt") + require.NoError(t, err) + + // Tasks created before PolicyName existed have an empty name; the + // summary must still carry the encoded policy id for client-side lookup. + owner := &ent.User{ID: 1} + task, err := NewImportTask(context.Background(), owner, "/src/path", false, "cloudreve://my/dst", 7, "") + require.NoError(t, err) + + summary := task.Summarize(hasher) + require.NotNil(t, summary) + require.Equal(t, "", summary.Props[SummaryKeyDstPolicyName]) + require.Equal(t, hashid.EncodePolicyID(hasher, 7), summary.Props[SummaryKeySrcDstPolicyID]) +} + +func TestImportTaskFromModelSummarize(t *testing.T) { + hasher, err := hashid.New("test-salt") + require.NoError(t, err) + + owner := &ent.User{ID: 1} + created, err := NewImportTask(context.Background(), owner, "/src/path", true, "cloudreve://my/dst", 7, "S3 Backup") + require.NoError(t, err) + + // Round-trip through the persisted model state. + restored := NewImportTaskFromModel(created.(*ImportTask).Task) + require.Equal(t, queue.ImportTaskType, restored.Type()) + + summary := restored.Summarize(hasher) + require.NotNil(t, summary) + require.Equal(t, "S3 Backup", summary.Props[SummaryKeyDstPolicyName]) +} diff --git a/pkg/filemanager/workflows/remote_download.go b/pkg/filemanager/workflows/remote_download.go index 9d089af0..d2245105 100644 --- a/pkg/filemanager/workflows/remote_download.go +++ b/pkg/filemanager/workflows/remote_download.go @@ -86,6 +86,7 @@ const ( SummaryKeySrcMultiple = "src_multiple" SummaryKeySrcDstPolicyID = "dst_policy_id" + SummaryKeyDstPolicyName = "dst_policy_name" SummaryKeyFailed = "failed" SummaryKeyTotal = "total" ) diff --git a/service/explorer/workflows.go b/service/explorer/workflows.go index 93f50ef7..f1f1f545 100644 --- a/service/explorer/workflows.go +++ b/service/explorer/workflows.go @@ -423,8 +423,15 @@ func (service *ImportWorkflowService) CreateImportTask(c *gin.Context) (*TaskRes return nil, serializer.NewError(serializer.CodeParamErr, "Invalid destination", err) } + // Resolve policy name for the task summary; best-effort, the task itself + // validates the policy at execution time. + var policyName string + if policy, err := dep.StoragePolicyClient().GetPolicyByID(c, service.PolicyID); err == nil { + policyName = policy.Name + } + // Create task - t, err := workflows.NewImportTask(c, owner, service.Src, service.Recursive, dst.Join(service.Dst).String(), service.PolicyID) + t, err := workflows.NewImportTask(c, owner, service.Src, service.Recursive, dst.Join(service.Dst).String(), service.PolicyID, policyName) if err != nil { return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create task", err) }