Add server components
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:28:09 +08:00
parent 7ecc6a8923
commit 079ee4eaeb
168 changed files with 37475 additions and 0 deletions
@@ -0,0 +1,385 @@
package synclegacy
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
_ "modernc.org/sqlite"
"ymhut-box/server/unified-management/internal/config"
"ymhut-box/server/unified-management/internal/db"
"ymhut-box/server/unified-management/internal/notices"
)
type Service struct {
cfg *config.Config
store *db.Store
notices *notices.Service
}
type Result struct {
Ok bool `json:"ok"`
DryRun bool `json:"dryRun"`
Paths map[string]any `json:"paths"`
Stats map[string]int `json:"stats"`
Warnings []string `json:"warnings"`
Errors []string `json:"errors"`
Started string `json:"startedAt"`
Finished string `json:"finishedAt"`
}
func New(cfg *config.Config, store *db.Store, noticeService *notices.Service) *Service {
return &Service{cfg: cfg, store: store, notices: noticeService}
}
func (s *Service) Preview(ctx context.Context) Result {
return s.run(ctx, true)
}
func (s *Service) Run(ctx context.Context) Result {
return s.run(ctx, false)
}
func (s *Service) run(ctx context.Context, dryRun bool) Result {
result := Result{
Ok: true,
DryRun: dryRun,
Paths: map[string]any{
"legacyUpdateDir": s.cfg.LegacyUpdateDir,
"legacyFeedbackDir": s.cfg.LegacyFeedbackDir,
"legacyUpdateNoticeDir": s.cfg.LegacyUpdateNoticeDir,
"updatePublicDir": s.cfg.UpdatePublicDir,
"updateNoticeDir": s.cfg.UpdateNoticeDir,
},
Stats: map[string]int{},
Started: db.Now(),
}
defer func() {
result.Finished = db.Now()
if len(result.Errors) > 0 {
result.Ok = false
}
}()
s.previewPath(&result, "legacy_update", s.cfg.LegacyUpdateDir)
s.previewPath(&result, "legacy_feedback", s.cfg.LegacyFeedbackDir)
s.previewPath(&result, "legacy_update_notice", s.cfg.LegacyUpdateNoticeDir)
if dryRun {
return result
}
if err := s.backupCurrent(); err != nil {
result.Errors = append(result.Errors, err.Error())
return result
}
s.syncUpdatePublic(&result)
s.syncNotices(ctx, &result)
s.syncFeedbackSQLite(&result)
_ = s.store.InsertAudit(db.AuditLog{Actor: "admin", Type: "legacy.sync", Target: "legacy-projects", Message: fmt.Sprintf("Legacy sync finished: ok=%v copied=%d imported=%d errors=%d", result.Ok, result.Stats["copiedFiles"], result.Stats["importedRows"], len(result.Errors))})
return result
}
func (s *Service) previewPath(result *Result, key, path string) {
info, err := os.Stat(path)
if err != nil {
result.Warnings = append(result.Warnings, key+": "+err.Error())
result.Stats["missingPaths"]++
return
}
if !info.IsDir() {
result.Warnings = append(result.Warnings, key+": path is not a directory")
result.Stats["missingPaths"]++
}
}
func (s *Service) backupCurrent() error {
backupRoot := filepath.Join(s.cfg.StorageDir, "backups", "legacy-sync-"+time.Now().UTC().Format("20060102-150405"))
for _, path := range []string{s.cfg.UpdatePublicDir, s.cfg.UpdateNoticeDir} {
if _, err := os.Stat(path); err == nil {
target := filepath.Join(backupRoot, filepath.Base(path))
if err := copyDir(path, target); err != nil {
return err
}
}
}
return nil
}
func (s *Service) syncUpdatePublic(result *Result) {
sourcePublic := filepath.Join(s.cfg.LegacyUpdateDir, "public")
if _, err := os.Stat(sourcePublic); err != nil {
result.Warnings = append(result.Warnings, "update public not found: "+err.Error())
return
}
for _, name := range []string{"update-info.json", "media-types.json", "tool-status.json", "modules.json"} {
source := filepath.Join(sourcePublic, name)
if _, err := os.Stat(source); err == nil {
if err := copyFile(source, filepath.Join(s.cfg.UpdatePublicDir, name)); err != nil {
result.Errors = append(result.Errors, err.Error())
} else {
result.Stats["copiedFiles"]++
}
}
}
sourceDownloads := filepath.Join(sourcePublic, "downloads")
if _, err := os.Stat(sourceDownloads); err == nil {
if err := copyDir(sourceDownloads, s.cfg.DownloadsDir); err != nil {
result.Errors = append(result.Errors, err.Error())
} else {
result.Stats["copiedDirectories"]++
}
}
}
func (s *Service) syncNotices(ctx context.Context, result *Result) {
for _, source := range []string{filepath.Join(s.cfg.LegacyUpdateDir, "update-notice"), s.cfg.LegacyUpdateNoticeDir} {
if _, err := os.Stat(source); err != nil {
continue
}
if err := copyDir(source, s.cfg.UpdateNoticeDir); err != nil {
result.Errors = append(result.Errors, err.Error())
continue
}
result.Stats["copiedDirectories"]++
}
if s.notices != nil {
if err := s.notices.Import(ctx); err != nil {
result.Errors = append(result.Errors, "notice import: "+err.Error())
} else {
result.Stats["noticeImports"]++
}
}
}
func (s *Service) syncFeedbackSQLite(result *Result) {
path := filepath.Join(s.cfg.LegacyFeedbackDir, "storage", "feedback.sqlite")
if _, err := os.Stat(path); err != nil {
result.Warnings = append(result.Warnings, "feedback sqlite not found: "+err.Error())
return
}
oldDB, err := sql.Open("sqlite", path)
if err != nil {
result.Errors = append(result.Errors, err.Error())
return
}
defer oldDB.Close()
s.importOldFeedbacks(oldDB, result)
s.importOldComments(oldDB, result)
s.importOldEvents(oldDB, result)
s.importOldTags(oldDB, result)
s.importOldMail(oldDB, result)
s.importOldWebhooks(oldDB, result)
}
func (s *Service) importOldFeedbacks(oldDB *sql.DB, result *Result) {
rows, err := oldDB.Query(`SELECT 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 FROM feedbacks`)
if err != nil {
result.Warnings = append(result.Warnings, "feedbacks: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var item db.Feedback
var mailSent int
if err := rows.Scan(&item.Code, &item.CreatedAt, &item.Title, &item.Type, &item.Severity, &item.Category, &item.Priority, &item.Contact, &item.Body, &item.Status, &item.StatusDetail, &item.Note, &item.PublicReply, &item.HandledBy, &item.Assignee, &item.DueAt, &item.ResolvedAt, &item.ArchivedAt, &item.SLALevel, &item.SourceChannel, &item.RiskScore, &item.Resolution, &item.PackagePath, &item.EncryptedPackagePath, &item.PackageSha256, &item.PlainPackageSha256, &item.RemoteAddr, &item.SummaryText, &item.IncludedFiles, &mailSent, &item.UpdatedAt, &item.LastActivityAt); err != nil {
result.Errors = append(result.Errors, "feedback scan: "+err.Error())
continue
}
item.MailSent = mailSent == 1
item.PackagePath = s.copyLegacyFeedbackFile(item.PackagePath, item.Code, result)
item.EncryptedPackagePath = s.copyLegacyFeedbackFile(item.EncryptedPackagePath, item.Code, result)
if err := s.store.InsertFeedback(item); err != nil && !strings.Contains(strings.ToLower(err.Error()), "unique") {
result.Errors = append(result.Errors, "feedback import "+item.Code+": "+err.Error())
} else {
result.Stats["importedRows"]++
}
}
}
func (s *Service) importOldComments(oldDB *sql.DB, result *Result) {
rows, err := oldDB.Query(`SELECT id, feedback_code, author, body, internal, created_at FROM feedback_comments`)
if err != nil {
result.Warnings = append(result.Warnings, "comments: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var comment db.FeedbackComment
var internal int
var oldID int64
if err := rows.Scan(&oldID, &comment.Code, &comment.Author, &comment.Body, &internal, &comment.CreatedAt); err != nil {
result.Errors = append(result.Errors, "comment scan: "+err.Error())
continue
}
comment.Internal = internal == 1
if _, err := s.store.InsertFeedbackComment(comment); err == nil {
result.Stats["importedRows"]++
}
}
}
func (s *Service) importOldEvents(oldDB *sql.DB, result *Result) {
rows, err := oldDB.Query(`SELECT id, feedback_code, event_type, actor, from_value, to_value, message, created_at FROM feedback_events`)
if err != nil {
result.Warnings = append(result.Warnings, "events: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var event db.LegacyFeedbackEvent
if err := rows.Scan(&event.ID, &event.FeedbackCode, &event.EventType, &event.Actor, &event.FromValue, &event.ToValue, &event.Message, &event.CreatedAt); err != nil {
result.Errors = append(result.Errors, "event scan: "+err.Error())
continue
}
if err := s.store.UpsertFeedbackEvent(event); err == nil {
result.Stats["importedRows"]++
}
}
}
func (s *Service) importOldTags(oldDB *sql.DB, result *Result) {
rows, err := oldDB.Query(`SELECT feedback_code, tag, created_at FROM feedback_tags`)
if err != nil {
result.Warnings = append(result.Warnings, "tags: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var code, tag, createdAt string
if err := rows.Scan(&code, &tag, &createdAt); err != nil {
continue
}
if err := s.store.UpsertFeedbackTag(code, tag, createdAt); err == nil {
result.Stats["importedRows"]++
}
}
}
func (s *Service) importOldMail(oldDB *sql.DB, result *Result) {
rows, err := oldDB.Query(`SELECT id, feedback_code, kind, status, to_address, subject, attachment_path, attachment_name, error_message, created_at, sent_at FROM mail_records`)
if err != nil {
result.Warnings = append(result.Warnings, "mail_records: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var item db.LegacyMailRecord
if err := rows.Scan(&item.ID, &item.FeedbackCode, &item.Kind, &item.Status, &item.ToAddress, &item.Subject, &item.AttachmentPath, &item.AttachmentName, &item.ErrorMessage, &item.CreatedAt, &item.SentAt); err != nil {
result.Errors = append(result.Errors, "mail scan: "+err.Error())
continue
}
item.AttachmentPath = s.copyLegacyFeedbackFile(item.AttachmentPath, item.FeedbackCode, result)
if err := s.store.UpsertMailRecord(item); err == nil {
result.Stats["importedRows"]++
}
}
}
func (s *Service) importOldWebhooks(oldDB *sql.DB, result *Result) {
rows, err := oldDB.Query(`SELECT id, webhook_name, event, status, attempts, response_code, error_message, payload_sha256, created_at, finished_at FROM webhook_deliveries`)
if err != nil {
result.Warnings = append(result.Warnings, "webhook_deliveries: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var id int64
var name, event, status, message, payload, createdAt, finishedAt string
var attempts, response int
if err := rows.Scan(&id, &name, &event, &status, &attempts, &response, &message, &payload, &createdAt, &finishedAt); err != nil {
continue
}
_ = s.store.InsertAudit(db.AuditLog{Actor: "legacy", Type: "webhook." + status, Target: name, Message: event + " " + message, CreatedAt: firstNonEmpty(createdAt, finishedAt, db.Now())})
result.Stats["importedRows"]++
}
}
func (s *Service) copyLegacyFeedbackFile(path, code string, result *Result) string {
if strings.TrimSpace(path) == "" {
return ""
}
source := path
if !filepath.IsAbs(source) {
source = filepath.Join(s.cfg.LegacyFeedbackDir, source)
}
info, err := os.Stat(source)
if err != nil || info.IsDir() {
return path
}
target := filepath.Join(s.cfg.StorageDir, "legacy-feedback", safeName(code), filepath.Base(source))
if err := copyFile(source, target); err != nil {
result.Warnings = append(result.Warnings, "copy attachment: "+err.Error())
return path
}
result.Stats["copiedFiles"]++
return target
}
func copyDir(source, target string) error {
sourceInfo, err := os.Stat(source)
if err != nil {
return err
}
if !sourceInfo.IsDir() {
return errors.New(source + " is not a directory")
}
return filepath.WalkDir(source, func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(source, path)
if err != nil {
return err
}
dest := filepath.Join(target, rel)
if entry.IsDir() {
return os.MkdirAll(dest, 0o750)
}
return copyFile(path, dest)
})
}
func copyFile(source, target string) error {
if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
return err
}
in, err := os.Open(source)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
func safeName(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "unknown"
}
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
return r
}
return '-'
}, value)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}