Files

265 lines
7.5 KiB
Go

package browser
import (
"ant-chrome/backend/internal/logger"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
const profileTrashRetention = 72 * time.Hour
// Delete 将配置移入回收站
func (m *Manager) Delete(profileId string) error {
log := logger.New("Browser")
m.InitData()
m.Mutex.Lock()
defer m.Mutex.Unlock()
profile, exists := m.Profiles[profileId]
if !exists {
log.Error("浏览器配置不存在", logger.F("profile_id", profileId))
return fmt.Errorf("profile not found")
}
deletedAt := time.Now().Format(time.RFC3339)
if m.ProfileDAO != nil {
if err := m.ProfileDAO.SoftDelete(profileId, deletedAt); err != nil {
log.Error("数据库移入回收站失败", logger.F("profile_id", profileId), logger.F("error", err))
return err
}
} else {
profile.DeletedAt = deletedAt
profile.UpdatedAt = deletedAt
delete(m.Profiles, profileId)
if err := m.SaveProfiles(); err != nil {
return err
}
}
profile.DeletedAt = deletedAt
profile.UpdatedAt = deletedAt
delete(m.Profiles, profileId)
log.Info("浏览器配置移入回收站", logger.F("profile_id", profileId))
return nil
}
// ListDeleted 获取回收站实例
func (m *Manager) ListDeleted() []Profile {
log := logger.New("Browser")
m.InitData()
m.Mutex.Lock()
defer m.Mutex.Unlock()
m.cleanupExpiredTrashLocked(log)
if m.ProfileDAO == nil {
return []Profile{}
}
profiles, err := m.ProfileDAO.ListDeleted()
if err != nil {
log.Error("查询回收站实例失败", logger.F("error", err))
return []Profile{}
}
list := make([]Profile, 0, len(profiles))
for _, profile := range profiles {
p := *profile
if m.CodeProvider != nil {
if code, err := m.CodeProvider.EnsureCode(p.ProfileId); err == nil {
p.LaunchCode = code
}
}
list = append(list, p)
}
return list
}
// Restore 从回收站恢复实例
func (m *Manager) Restore(profileId string) (*Profile, error) {
log := logger.New("Browser")
m.InitData()
m.Mutex.Lock()
defer m.Mutex.Unlock()
if m.ProfileDAO == nil {
return nil, fmt.Errorf("当前环境不支持回收站恢复")
}
profile, err := m.ProfileDAO.GetById(profileId)
if err != nil {
return nil, err
}
if strings.TrimSpace(profile.DeletedAt) == "" {
return nil, fmt.Errorf("实例不在回收站")
}
if err := m.ProfileDAO.Restore(profileId); err != nil {
return nil, err
}
profile.DeletedAt = ""
profile.UpdatedAt = time.Now().Format(time.RFC3339)
profile.CoreId = normalizeProfileCoreID(profile.CoreId)
m.Profiles[profile.ProfileId] = profile
log.Info("实例已从回收站恢复", logger.F("profile_id", profileId))
return profile, nil
}
// PermanentlyDelete 从回收站彻底删除实例及其关联数据
func (m *Manager) PermanentlyDelete(profileId string) error {
log := logger.New("Browser")
m.InitData()
m.Mutex.Lock()
defer m.Mutex.Unlock()
if m.ProfileDAO == nil {
return fmt.Errorf("当前环境不支持回收站物理删除")
}
profile, err := m.ProfileDAO.GetById(profileId)
if err != nil {
return err
}
if strings.TrimSpace(profile.DeletedAt) == "" {
return fmt.Errorf("只能彻底删除回收站内的实例")
}
if err := m.deleteProfileRelatedDataLocked(log, profile); err != nil {
return err
}
if err := m.ProfileDAO.Delete(profileId); err != nil {
return err
}
log.Info("回收站实例已彻底删除", logger.F("profile_id", profileId))
return nil
}
// CleanupExpiredTrash 清理超过保留期的回收站实例
func (m *Manager) CleanupExpiredTrash() error {
log := logger.New("Browser")
m.InitData()
m.Mutex.Lock()
defer m.Mutex.Unlock()
return m.cleanupExpiredTrashLocked(log)
}
func (m *Manager) cleanupExpiredTrashLocked(log *logger.Logger) error {
if m.ProfileDAO == nil {
return nil
}
expiredBefore := time.Now().Add(-profileTrashRetention).Format(time.RFC3339)
expired, err := m.ProfileDAO.ListExpiredDeleted(expiredBefore)
if err != nil {
log.Error("清理过期回收站实例失败", logger.F("error", err))
return err
}
cleaned := 0
for _, profile := range expired {
if err := m.deleteProfileRelatedDataLocked(log, profile); err != nil {
log.Error("清理过期回收站实例关联数据失败", logger.F("profile_id", profile.ProfileId), logger.F("error", err))
continue
}
if err := m.ProfileDAO.Delete(profile.ProfileId); err != nil {
log.Error("删除过期回收站实例记录失败", logger.F("profile_id", profile.ProfileId), logger.F("error", err))
continue
}
cleaned++
}
if cleaned > 0 {
log.Info("过期回收站实例已清理", logger.F("count", cleaned))
}
return nil
}
func (m *Manager) deleteProfileRelatedDataLocked(log *logger.Logger, profile *Profile) error {
if profile == nil {
return nil
}
var firstErr error
if m.CodeProvider != nil {
if err := m.CodeProvider.Remove(profile.ProfileId); err != nil && firstErr == nil {
firstErr = err
}
}
if m.ExtensionDAO != nil {
if err := m.ExtensionDAO.DeleteProfileSettings(profile.ProfileId); err != nil {
log.Error("删除实例插件配置失败", logger.F("profile_id", profile.ProfileId), logger.F("error", err))
if firstErr == nil {
firstErr = err
}
}
}
userDataDir := m.ResolveUserDataDir(profile)
if err := m.deleteProfileUserDataDir(userDataDir); err != nil {
log.Error("删除实例数据目录失败", logger.F("profile_id", profile.ProfileId), logger.F("dir", userDataDir), logger.F("error", err))
if firstErr == nil {
firstErr = err
}
}
if err := m.deleteProfileSnapshotDir(profile.ProfileId); err != nil {
log.Error("删除实例快照目录失败", logger.F("profile_id", profile.ProfileId), logger.F("error", err))
if firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (m *Manager) deleteProfileSnapshotDir(profileId string) error {
profileId = strings.TrimSpace(profileId)
if profileId == "" {
return nil
}
dataRoot, err := filepath.Abs(m.ResolveRelativePath("data"))
if err != nil {
return fmt.Errorf("解析数据根目录失败: %w", err)
}
snapshotRoot := filepath.Join(dataRoot, "snapshots")
target, err := filepath.Abs(filepath.Join(snapshotRoot, profileId))
if err != nil {
return fmt.Errorf("解析快照目录失败: %w", err)
}
dataRoot = filepath.Clean(dataRoot)
snapshotRoot = filepath.Clean(snapshotRoot)
target = filepath.Clean(target)
if samePath(target, snapshotRoot) || samePath(target, dataRoot) || !isPathInside(target, snapshotRoot) {
return nil
}
if err := os.RemoveAll(target); err != nil {
return fmt.Errorf("删除快照目录失败: %w", err)
}
return nil
}
func (m *Manager) deleteProfileUserDataDir(userDataDir string) error {
userDataDir = strings.TrimSpace(userDataDir)
if userDataDir == "" {
return nil
}
target, err := filepath.Abs(userDataDir)
if err != nil {
return fmt.Errorf("解析实例数据目录失败: %w", err)
}
root := strings.TrimSpace(m.Config.Browser.UserDataRoot)
if root == "" {
root = "data"
}
rootAbs, err := filepath.Abs(m.ResolveRelativePath(root))
if err != nil {
return fmt.Errorf("解析用户数据根目录失败: %w", err)
}
target = filepath.Clean(target)
rootAbs = filepath.Clean(rootAbs)
if samePath(target, rootAbs) || !isPathInside(target, rootAbs) {
return nil
}
if err := os.RemoveAll(target); err != nil {
return fmt.Errorf("删除实例数据目录失败: %w", err)
}
return nil
}
func samePath(a string, b string) bool {
return strings.EqualFold(filepath.Clean(a), filepath.Clean(b))
}
func isPathInside(path string, parent string) bool {
rel, err := filepath.Rel(parent, path)
if err != nil || rel == "." || rel == "" {
return false
}
return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}