diff --git a/internal/store/accounts.go b/internal/store/accounts.go index 015bdaa..ef94512 100644 --- a/internal/store/accounts.go +++ b/internal/store/accounts.go @@ -6,19 +6,21 @@ import ( ) type Account struct { - ID int64 - TaskID int64 - SrcLogin string - SrcPassEnc string - DstLogin string - DstPassEnc string - TestSrcStatus string - TestDstStatus string - Status string - Copied int64 - Skipped int64 - Errors int64 - LastError string + ID int64 + TaskID int64 + SrcLogin string + SrcPassEnc string + DstLogin string + DstPassEnc string + TestSrcStatus string + TestDstStatus string + Status string + Copied int64 + Skipped int64 + Errors int64 + LastError string + FolderMapping map[string]string + ExcludedFolders []string } func (s *Store) CreateAccount(ctx context.Context, a Account) (int64, error) { @@ -39,7 +41,8 @@ func (s *Store) DeleteAccount(ctx context.Context, id int64) error { func (s *Store) ListAccountsByTask(ctx context.Context, taskID int64) ([]Account, error) { rows, err := s.Pool.Query(ctx, `SELECT id, task_id, src_login, src_pass_enc, dst_login, dst_pass_enc, - test_src_status, test_dst_status, status, copied_count, skipped_count, error_count, last_error + test_src_status, test_dst_status, status, copied_count, skipped_count, + error_count, last_error, folder_mapping, excluded_folders FROM accounts WHERE task_id=$1 ORDER BY id`, taskID) if err != nil { return nil, err @@ -49,7 +52,8 @@ func (s *Store) ListAccountsByTask(ctx context.Context, taskID int64) ([]Account for rows.Next() { var a Account if err := rows.Scan(&a.ID, &a.TaskID, &a.SrcLogin, &a.SrcPassEnc, &a.DstLogin, &a.DstPassEnc, - &a.TestSrcStatus, &a.TestDstStatus, &a.Status, &a.Copied, &a.Skipped, &a.Errors, &a.LastError); err != nil { + &a.TestSrcStatus, &a.TestDstStatus, &a.Status, &a.Copied, &a.Skipped, &a.Errors, &a.LastError, + &a.FolderMapping, &a.ExcludedFolders); err != nil { return nil, err } out = append(out, a) @@ -95,3 +99,19 @@ func (s *Store) IncAccountCounters(ctx context.Context, id, copied, skipped, err id, copied, skipped, errs) return err } + +// SetAccountFolderMapping persists an account's per-folder rename map and the +// set of source folders to skip. nil is normalized to empty so JSONB stays +// '{}' / '[]' rather than null. +func (s *Store) SetAccountFolderMapping(ctx context.Context, id int64, mapping map[string]string, excluded []string) error { + if mapping == nil { + mapping = map[string]string{} + } + if excluded == nil { + excluded = []string{} + } + _, err := s.Pool.Exec(ctx, + `UPDATE accounts SET folder_mapping=$2, excluded_folders=$3 WHERE id=$1`, + id, mapping, excluded) + return err +} diff --git a/internal/store/accounts_test.go b/internal/store/accounts_test.go index 0057e21..02277a5 100644 --- a/internal/store/accounts_test.go +++ b/internal/store/accounts_test.go @@ -64,3 +64,31 @@ func TestResetAccountCounters(t *testing.T) { a.Copied, a.Skipped, a.Errors) } } + +func TestSetAccountFolderMapping(t *testing.T) { + s := testStore(t) + ctx := context.Background() + epSrc, _ := s.CreateEndpoint(ctx, Endpoint{RoleLabel: "src", Host: "a", Port: 993, TLSMode: "ssl"}) + epDst, _ := s.CreateEndpoint(ctx, Endpoint{RoleLabel: "dst", Host: "b", Port: 993, TLSMode: "ssl"}) + taskID, _ := s.CreateTask(ctx, Task{Name: "t", SrcEndpointID: epSrc, DstEndpointID: epDst}) + accID, _ := s.CreateAccount(ctx, Account{TaskID: taskID, SrcLogin: "u", SrcPassEnc: "x", DstLogin: "u2", DstPassEnc: "y"}) + + // Fresh account defaults: empty map, empty exclusions (not nil after scan). + accs, _ := s.ListAccountsByTask(ctx, taskID) + if len(accs) != 1 || len(accs[0].FolderMapping) != 0 || len(accs[0].ExcludedFolders) != 0 { + t.Fatalf("defaults: map=%v excl=%v", accs[0].FolderMapping, accs[0].ExcludedFolders) + } + + if err := s.SetAccountFolderMapping(ctx, accID, + map[string]string{"Спам": "Spam"}, []string{"Trash"}); err != nil { + t.Fatalf("set: %v", err) + } + accs, _ = s.ListAccountsByTask(ctx, taskID) + a := accs[0] + if a.FolderMapping["Спам"] != "Spam" { + t.Fatalf("mapping not persisted: %v", a.FolderMapping) + } + if len(a.ExcludedFolders) != 1 || a.ExcludedFolders[0] != "Trash" { + t.Fatalf("excluded not persisted: %v", a.ExcludedFolders) + } +} diff --git a/migrations/0003_account_folder_mapping.down.sql b/migrations/0003_account_folder_mapping.down.sql new file mode 100644 index 0000000..9a7c8dc --- /dev/null +++ b/migrations/0003_account_folder_mapping.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE accounts DROP COLUMN excluded_folders; +ALTER TABLE accounts DROP COLUMN folder_mapping; diff --git a/migrations/0003_account_folder_mapping.up.sql b/migrations/0003_account_folder_mapping.up.sql new file mode 100644 index 0000000..941b958 --- /dev/null +++ b/migrations/0003_account_folder_mapping.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE accounts ADD COLUMN folder_mapping JSONB NOT NULL DEFAULT '{}'; +ALTER TABLE accounts ADD COLUMN excluded_folders JSONB NOT NULL DEFAULT '[]'; + +-- Carry the existing task-wide mapping onto its accounts so behavior is preserved. +UPDATE accounts a SET folder_mapping = t.folder_mapping + FROM tasks t WHERE a.task_id = t.id AND t.folder_mapping <> '{}'::jsonb;