121 lines
4.1 KiB
Go
121 lines
4.1 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type SyncResult struct {
|
|
Direction string `json:"direction"`
|
|
Tables map[string]int `json:"tables"`
|
|
FinishedAt string `json:"finishedAt"`
|
|
}
|
|
|
|
type tableSpec struct {
|
|
Name string
|
|
Columns []string
|
|
Conflict []string
|
|
}
|
|
|
|
var syncTables = []tableSpec{
|
|
{"feedbacks", []string{"code", "received_at", "title", "type", "severity", "category", "priority", "contact", "body", "status", "status_detail", "note", "public_reply", "handled_by", "assignee", "due_at", "resolved_at", "archived_at", "sla_level", "source_channel", "risk_score", "resolution", "package_path", "encrypted_package_path", "package_sha256", "plain_package_sha256", "remote_addr", "summary_text", "included_files", "mail_sent", "updated_at", "last_activity_at"}, []string{"code"}},
|
|
{"mail_records", []string{"id", "feedback_code", "kind", "status", "to_address", "subject", "plain_body", "html_body", "attachment_path", "attachment_name", "error_message", "created_at", "sent_at"}, []string{"id"}},
|
|
{"feedback_events", []string{"id", "feedback_code", "event_type", "actor", "from_value", "to_value", "message", "created_at"}, []string{"id"}},
|
|
{"feedback_comments", []string{"id", "feedback_code", "author", "body", "internal", "created_at"}, []string{"id"}},
|
|
{"feedback_tags", []string{"feedback_code", "tag", "created_at"}, []string{"feedback_code", "tag"}},
|
|
{"audit_logs", []string{"id", "actor", "type", "target", "message", "ip", "user_agent", "created_at"}, []string{"id"}},
|
|
{"webhook_deliveries", []string{"id", "webhook_name", "event", "status", "attempts", "response_code", "error_message", "payload_sha256", "created_at", "finished_at"}, []string{"id"}},
|
|
}
|
|
|
|
func (s *Store) ImportSQLiteToRemote() (SyncResult, error) {
|
|
s.mu.RLock()
|
|
remote := s.remoteDB
|
|
remoteDialect := s.remoteDialect
|
|
local := s.localDB
|
|
localDialect := s.localDialect
|
|
s.mu.RUnlock()
|
|
if remote == nil {
|
|
return SyncResult{}, fmt.Errorf("remote database is not configured")
|
|
}
|
|
result, err := copyAllTables(local, localDialect, remote, remoteDialect, "sqlite_to_remote")
|
|
s.setSyncStatus(result, err)
|
|
return result, err
|
|
}
|
|
|
|
func (s *Store) SyncNow() (SyncResult, error) {
|
|
return s.syncRemoteToSQLite()
|
|
}
|
|
|
|
func (s *Store) syncRemoteToSQLite() (SyncResult, error) {
|
|
s.mu.RLock()
|
|
remote := s.remoteDB
|
|
remoteDialect := s.remoteDialect
|
|
local := s.localDB
|
|
localDialect := s.localDialect
|
|
s.mu.RUnlock()
|
|
if remote == nil {
|
|
return SyncResult{}, nil
|
|
}
|
|
result, err := copyAllTables(remote, remoteDialect, local, localDialect, "remote_to_sqlite")
|
|
s.setSyncStatus(result, err)
|
|
return result, err
|
|
}
|
|
|
|
func (s *Store) setSyncStatus(result SyncResult, err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err != nil {
|
|
s.status.LastSyncError = err.Error()
|
|
return
|
|
}
|
|
if result.FinishedAt != "" {
|
|
s.status.LastSyncAt = result.FinishedAt
|
|
}
|
|
s.status.LastSyncError = ""
|
|
}
|
|
|
|
func copyAllTables(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, direction string) (SyncResult, error) {
|
|
result := SyncResult{Direction: direction, Tables: map[string]int{}, FinishedAt: Now()}
|
|
for _, table := range syncTables {
|
|
copied, err := copyTable(src, srcDialect, dst, dstDialect, table)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Tables[table.Name] = copied
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func copyTable(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, spec tableSpec) (int, error) {
|
|
selectSQL := "SELECT " + strings.Join(spec.Columns, ", ") + " FROM " + spec.Name
|
|
rows, err := src.Query(srcDialect.rebind(selectSQL))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
insertSQL := dstDialect.rebind(dstDialect.upsert(spec.Name, spec.Columns, spec.Conflict))
|
|
count := 0
|
|
for rows.Next() {
|
|
values := make([]any, len(spec.Columns))
|
|
ptrs := make([]any, len(spec.Columns))
|
|
for index := range values {
|
|
ptrs[index] = &values[index]
|
|
}
|
|
if err := rows.Scan(ptrs...); err != nil {
|
|
return count, err
|
|
}
|
|
for index, value := range values {
|
|
if bytes, ok := value.([]byte); ok {
|
|
values[index] = string(bytes)
|
|
}
|
|
}
|
|
if _, err := dst.Exec(insertSQL, values...); err != nil {
|
|
return count, err
|
|
}
|
|
count++
|
|
}
|
|
return count, rows.Err()
|
|
}
|