package app import ( "crypto/hmac" "encoding/hex" "encoding/json" "errors" "io" "net/http" "os" "path/filepath" "strings" ) type viewerLegacyImport struct { Version int `json:"version"` Viewer string `json:"viewer"` Completed bool `json:"completed"` } func (manager *viewerManager) legacyImport() (viewerLegacyImport, error) { var state viewerLegacyImport file, err := os.Open(filepath.Join(manager.directory, "viewer-legacy-import.json")) if errors.Is(err, os.ErrNotExist) { return state, nil } if err != nil { return state, err } defer file.Close() decoder := json.NewDecoder(io.LimitReader(file, 4096)) if err = decoder.Decode(&state); err != nil { return state, err } id, idErr := hex.DecodeString(state.Viewer) if state.Version != 1 || idErr != nil || len(id) != 32 || decoder.Decode(new(any)) != io.EOF { return state, errors.New("旧版记录恢复状态无效,原文件已保留") } return state, nil } func (manager *viewerManager) hasLegacyRecords() (bool, error) { found := false for _, name := range []string{"playback-history.json", "following.json"} { info, err := os.Stat(filepath.Join(manager.directory, name)) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { return false, err } if !info.Mode().IsRegular() { return false, errors.New("旧版记录路径不是普通文件") } found = true } return found, nil } func (manager *viewerManager) legacyOwner(id string) string { store := manager.accountStore() store.mu.Lock() defer store.mu.Unlock() if imported, exists := store.state.GuestImports[id]; exists { if account := store.state.Accounts[imported.Account]; account.ID != "" { return account.viewerID() } } return id } func (manager *viewerManager) legacyAvailable(id string) (bool, error) { manager.legacyMu.Lock() defer manager.legacyMu.Unlock() state, err := manager.legacyImport() if err != nil || state.Completed || state.Viewer != "" && manager.legacyOwner(state.Viewer) != id { return false, err } return manager.hasLegacyRecords() } func (manager *viewerManager) legacyCode() string { return hex.EncodeToString(manager.signature("legacy-records")) } func (manager *viewerManager) prepareLegacyCode() (bool, error) { manager.legacyMu.Lock() defer manager.legacyMu.Unlock() state, err := manager.legacyImport() if err != nil || state.Completed { return false, err } available, err := manager.hasLegacyRecords() if err != nil || !available { return false, err } err = writeNewViewerFile(filepath.Join(manager.directory, "viewer-legacy-code"), []byte(manager.legacyCode()+"\n")) if errors.Is(err, os.ErrExist) { err = nil } return true, err } func (manager *viewerManager) saveLegacyImport(state viewerLegacyImport, create bool) error { data, err := json.Marshal(state) if err != nil { return err } path := filepath.Join(manager.directory, "viewer-legacy-import.json") if create { return writeNewViewerFile(path, data) } file, err := os.CreateTemp(manager.directory, ".viewer-legacy-*.json") if err != nil { return err } defer os.Remove(file.Name()) if _, err = file.Write(data); err == nil { err = file.Sync() } if closeErr := file.Close(); err == nil { err = closeErr } if err != nil { return err } return os.Rename(file.Name(), path) } func (app *UIApp) handleViewerLegacy(writer http.ResponseWriter, request *http.Request) { var input struct { Code string `json:"code"` } if !readPlaybackRequest(writer, request, &input) { return } viewer := requestViewer(writer, request) if viewer == nil { return } manager := viewer.manager code := strings.TrimSpace(input.Code) if len(code) != 64 || !hmac.Equal([]byte(code), []byte(manager.legacyCode())) { writeJSON(writer, http.StatusForbidden, map[string]string{"error": "恢复码不正确,请从服务所在电脑的数据目录获取"}) return } manager.legacyMu.Lock() defer manager.legacyMu.Unlock() state, err := manager.legacyImport() if err == nil && state.Viewer != "" && manager.legacyOwner(state.Viewer) != viewer.id { writeJSON(writer, http.StatusConflict, map[string]string{"error": "旧版记录已经归属其他账号或浏览器,不能重复领取"}) return } if err == nil && state.Completed { writeJSON(writer, http.StatusOK, map[string]bool{"ok": true}) return } if err == nil { var available bool available, err = manager.hasLegacyRecords() if err == nil && !available { writeJSON(writer, http.StatusNotFound, map[string]string{"error": "没有待恢复的旧版记录"}) return } } var history []playbackHistoryEntry var following []followingEntry if err == nil { history, err = newPlaybackHistoryStore(manager.directory).list() } if err == nil { following, err = newFollowingStore(manager.directory).list() } if err == nil { _, err = viewer.playbackHistory().list() } if err == nil { _, err = viewer.followingStore().list() } if err == nil && state.Viewer != viewer.id { create := state.Viewer == "" state = viewerLegacyImport{Version: 1, Viewer: viewer.id} err = manager.saveLegacyImport(state, create) } if err == nil { err = viewer.followingStore().importLegacy(following) } if err == nil { err = viewer.playbackHistory().importLegacy(history) } if err == nil { state.Completed = true err = manager.saveLegacyImport(state, false) } if err != nil { writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "旧版记录尚未全部恢复,可在当前账号或浏览器重试:" + publicError(err).Error()}) return } writeJSON(writer, http.StatusOK, map[string]bool{"ok": true}) } func (store *playbackHistoryStore) importLegacy(entries []playbackHistoryEntry) error { store.mu.Lock() if store.loadErr != nil { err := store.loadErr store.mu.Unlock() return err } for _, entry := range entries { if _, exists := store.entries[entry.DramaID]; exists || !entry.WatchedAt.After(store.cleared) || !entry.WatchedAt.After(store.deleted[entry.DramaID]) { continue } store.entries[entry.DramaID] = entry store.revision++ } store.trimLocked() store.mu.Unlock() return store.flush() } func (store *followingStore) importLegacy(entries []followingEntry) error { store.mu.Lock() defer store.mu.Unlock() if store.loadErr != nil { return store.loadErr } next := make(map[string]followingEntry, len(store.entries)) for id, entry := range store.entries { next[id] = entry } for _, entry := range entries { if _, exists := next[entry.DramaID]; !exists { next[entry.DramaID] = entry } } if len(next) > followingLimit { return errFollowingLimit } if len(next) == len(store.entries) { return nil } if err := store.write(followingFile{Version: 1, Entries: followingEntries(next)}); err != nil { return err } store.entries = next return nil }