package migrator import ( "context" "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/application/migrator/conf" "github.com/cloudreve/Cloudreve/v4/application/migrator/model" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/util" ) // State stores the migration progress type State struct { PolicyIDs map[int]bool `json:"policy_ids,omitempty"` LocalPolicyIDs map[int]bool `json:"local_policy_ids,omitempty"` UserIDs map[int]bool `json:"user_ids,omitempty"` FolderIDs map[int]bool `json:"folder_ids,omitempty"` EntitySources map[string]int `json:"entity_sources,omitempty"` LastFolderID int `json:"last_folder_id,omitempty"` Step int `json:"step,omitempty"` UserOffset int `json:"user_offset,omitempty"` FolderOffset int `json:"folder_offset,omitempty"` FileOffset int `json:"file_offset,omitempty"` ShareOffset int `json:"share_offset,omitempty"` GiftCodeOffset int `json:"gift_code_offset,omitempty"` DirectLinkOffset int `json:"direct_link_offset,omitempty"` WebdavOffset int `json:"webdav_offset,omitempty"` StoragePackOffset int `json:"storage_pack_offset,omitempty"` FileConflictRename map[uint]string `json:"file_conflict_rename,omitempty"` FolderParentOffset int `json:"folder_parent_offset,omitempty"` ThumbSuffix string `json:"thumb_suffix,omitempty"` V3AvatarPath string `json:"v3_avatar_path,omitempty"` } // Step identifiers for migration phases const ( StepInitial = 0 StepSchema = 1 StepSettings = 2 StepNode = 3 StepPolicy = 4 StepGroup = 5 StepUser = 6 StepFolders = 7 StepFolderParent = 8 StepFile = 9 StepShare = 10 StepDirectLink = 11 Step_CommunityPlaceholder1 = 12 Step_CommunityPlaceholder2 = 13 StepAvatar = 14 StepWebdav = 15 StepCompleted = 16 StateFileName = "migration_state.json" ) type Migrator struct { dep dependency.Dep l logging.Logger v4client *ent.Client state *State statePath string } func NewMigrator(dep dependency.Dep, v3ConfPath string) (*Migrator, error) { m := &Migrator{ dep: dep, l: dep.Logger(), state: &State{ PolicyIDs: make(map[int]bool), UserIDs: make(map[int]bool), Step: StepInitial, UserOffset: 0, FolderOffset: 0, }, } // Determine state file path configDir := filepath.Dir(v3ConfPath) m.statePath = filepath.Join(configDir, StateFileName) // Try to load existing state if util.Exists(m.statePath) { m.l.Info("Found existing migration state file, loading from %s", m.statePath) if err := m.loadState(); err != nil { return nil, fmt.Errorf("failed to load migration state: %w", err) } stepName := "unknown" switch m.state.Step { case StepInitial: stepName = "initial" case StepSchema: stepName = "schema creation" case StepSettings: stepName = "settings migration" case StepNode: stepName = "node migration" case StepPolicy: stepName = "policy migration" case StepGroup: stepName = "group migration" case StepUser: stepName = "user migration" case StepFolders: stepName = "folders migration" case StepCompleted: stepName = "completed" case StepWebdav: stepName = "webdav migration" case StepAvatar: stepName = "avatar migration" } m.l.Info("Resumed migration from step %d (%s)", m.state.Step, stepName) // Log batch information if applicable if m.state.Step == StepUser && m.state.UserOffset > 0 { m.l.Info("Will resume user migration from batch offset %d", m.state.UserOffset) } if m.state.Step == StepFolders && m.state.FolderOffset > 0 { m.l.Info("Will resume folder migration from batch offset %d", m.state.FolderOffset) } } err := conf.Init(m.dep.Logger(), v3ConfPath) if err != nil { return nil, err } err = model.Init() if err != nil { return nil, err } v4client, err := inventory.NewRawEntClient(m.l, m.dep.ConfigProvider()) if err != nil { return nil, err } m.v4client = v4client return m, nil } // saveState persists migration state to file func (m *Migrator) saveState() error { data, err := json.Marshal(m.state) if err != nil { return fmt.Errorf("failed to marshal state: %w", err) } return os.WriteFile(m.statePath, data, 0644) } // loadState reads migration state from file func (m *Migrator) loadState() error { data, err := os.ReadFile(m.statePath) if err != nil { return fmt.Errorf("failed to read state file: %w", err) } return json.Unmarshal(data, m.state) } // updateStep updates current step and persists state func (m *Migrator) updateStep(step int) error { m.state.Step = step return m.saveState() } func (m *Migrator) Migrate() error { // Continue from the current step if m.state.Step <= StepSchema { m.l.Info("Creating basic v4 table schema...") if err := m.v4client.Schema.Create(context.Background()); err != nil { return fmt.Errorf("failed creating schema resources: %w", err) } if err := m.updateStep(StepSettings); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepSettings { if err := m.migrateSettings(); err != nil { return err } if err := m.updateStep(StepNode); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepNode { if err := m.migrateNode(); err != nil { return err } if err := m.updateStep(StepPolicy); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepPolicy { allPolicyIDs, err := m.migratePolicy() if err != nil { return err } m.state.PolicyIDs = allPolicyIDs if err := m.updateStep(StepGroup); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepGroup { if err := m.migrateGroup(); err != nil { return err } if err := m.updateStep(StepUser); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepUser { if err := m.migrateUser(); err != nil { m.saveState() return err } // Reset user offset after completion m.state.UserOffset = 0 if err := m.updateStep(StepFolders); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepFolders { if err := m.migrateFolders(); err != nil { m.saveState() return err } // Reset folder offset after completion m.state.FolderOffset = 0 if err := m.updateStep(StepFolderParent); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepFolderParent { if err := m.migrateFolderParent(); err != nil { return err } if err := m.updateStep(StepFile); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepFile { if err := m.migrateFile(); err != nil { return err } if err := m.updateStep(StepShare); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepShare { if err := m.migrateShare(); err != nil { return err } if err := m.updateStep(StepDirectLink); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepDirectLink { if err := m.migrateDirectLink(); err != nil { return err } if err := m.updateStep(StepAvatar); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepAvatar { if err := migrateAvatars(m); err != nil { return err } if err := m.updateStep(StepWebdav); err != nil { return fmt.Errorf("failed to update step: %w", err) } } if m.state.Step <= StepWebdav { if err := m.migrateWebdav(); err != nil { return err } if err := m.updateStep(StepCompleted); err != nil { return fmt.Errorf("failed to update step: %w", err) } } m.l.Info("Migration completed successfully") return nil } func formatTime(t time.Time) time.Time { newTime := time.UnixMilli(t.UnixMilli()) return newTime }