feat(store): per-account folder_mapping + excluded_folders columns

This commit is contained in:
2026-07-03 11:39:21 +07:00
parent e7870c6aa4
commit d6d17ee544
4 changed files with 71 additions and 15 deletions
+35 -15
View File
@@ -6,19 +6,21 @@ import (
) )
type Account struct { type Account struct {
ID int64 ID int64
TaskID int64 TaskID int64
SrcLogin string SrcLogin string
SrcPassEnc string SrcPassEnc string
DstLogin string DstLogin string
DstPassEnc string DstPassEnc string
TestSrcStatus string TestSrcStatus string
TestDstStatus string TestDstStatus string
Status string Status string
Copied int64 Copied int64
Skipped int64 Skipped int64
Errors int64 Errors int64
LastError string LastError string
FolderMapping map[string]string
ExcludedFolders []string
} }
func (s *Store) CreateAccount(ctx context.Context, a Account) (int64, error) { 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) { func (s *Store) ListAccountsByTask(ctx context.Context, taskID int64) ([]Account, error) {
rows, err := s.Pool.Query(ctx, rows, err := s.Pool.Query(ctx,
`SELECT id, task_id, src_login, src_pass_enc, dst_login, dst_pass_enc, `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) FROM accounts WHERE task_id=$1 ORDER BY id`, taskID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -49,7 +52,8 @@ func (s *Store) ListAccountsByTask(ctx context.Context, taskID int64) ([]Account
for rows.Next() { for rows.Next() {
var a Account var a Account
if err := rows.Scan(&a.ID, &a.TaskID, &a.SrcLogin, &a.SrcPassEnc, &a.DstLogin, &a.DstPassEnc, 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 return nil, err
} }
out = append(out, a) out = append(out, a)
@@ -95,3 +99,19 @@ func (s *Store) IncAccountCounters(ctx context.Context, id, copied, skipped, err
id, copied, skipped, errs) id, copied, skipped, errs)
return err 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
}
+28
View File
@@ -64,3 +64,31 @@ func TestResetAccountCounters(t *testing.T) {
a.Copied, a.Skipped, a.Errors) 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)
}
}
@@ -0,0 +1,2 @@
ALTER TABLE accounts DROP COLUMN excluded_folders;
ALTER TABLE accounts DROP COLUMN folder_mapping;
@@ -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;