Files
djk/internal/app/ui_viewer_legacy.go
T

248 lines
6.7 KiB
Go

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
}