WitNote / internal /profiles /profiles.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
package profiles
import (
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/pinchtab/pinchtab/internal/bridge"
"github.com/pinchtab/pinchtab/internal/idutil"
)
var idMgr = idutil.NewManager()
func profileID(name string) string {
return idMgr.ProfileID(name)
}
// ValidateProfileName checks that a profile name is safe and doesn't contain
// path traversal characters like "..", "/", or "\".
func ValidateProfileName(name string) error {
if name == "" {
return fmt.Errorf("profile name cannot be empty")
}
if strings.Contains(name, "..") {
return fmt.Errorf("profile name cannot contain '..'")
}
if strings.ContainsAny(name, "/\\") {
return fmt.Errorf("profile name cannot contain '/' or '\\'")
}
return nil
}
type ProfileManager struct {
baseDir string
tracker *ActionTracker
mu sync.RWMutex
}
type ProfileMeta struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
UseWhen string `json:"useWhen,omitempty"`
Description string `json:"description,omitempty"`
}
type ProfileDetailedInfo struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Path string `json:"path"`
CreatedAt time.Time `json:"createdAt"`
SizeMB float64 `json:"sizeMB"`
Source string `json:"source,omitempty"`
ChromeProfileName string `json:"chromeProfileName,omitempty"`
AccountEmail string `json:"accountEmail,omitempty"`
AccountName string `json:"accountName,omitempty"`
HasAccount bool `json:"hasAccount,omitempty"`
UseWhen string `json:"useWhen,omitempty"`
Description string `json:"description,omitempty"`
}
func NewProfileManager(baseDir string) *ProfileManager {
_ = os.MkdirAll(baseDir, 0755)
return &ProfileManager{
baseDir: baseDir,
tracker: NewActionTracker(),
}
}
func (pm *ProfileManager) findProfileDirByName(name string) (string, error) {
direct := filepath.Join(pm.baseDir, name)
if info, err := os.Stat(direct); err == nil && info.IsDir() {
return direct, nil
}
entries, err := os.ReadDir(pm.baseDir)
if err != nil {
return "", err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
dir := filepath.Join(pm.baseDir, entry.Name())
if entry.Name() == profileID(name) {
return dir, nil
}
meta := readProfileMeta(dir)
if meta.Name == name {
return dir, nil
}
}
return "", fmt.Errorf("profile %q not found", name)
}
func (pm *ProfileManager) profileDir(name string) (string, error) {
if err := ValidateProfileName(name); err != nil {
return "", err
}
pm.mu.RLock()
defer pm.mu.RUnlock()
return pm.findProfileDirByName(name)
}
func (pm *ProfileManager) Exists(name string) bool {
_, err := pm.profileDir(name)
return err == nil
}
func (pm *ProfileManager) ProfilePath(name string) (string, error) {
return pm.profileDir(name)
}
func (pm *ProfileManager) List() ([]bridge.ProfileInfo, error) {
pm.mu.RLock()
defer pm.mu.RUnlock()
entries, err := os.ReadDir(pm.baseDir)
if err != nil {
return nil, err
}
profiles := []bridge.ProfileInfo{}
skip := map[string]bool{"bin": true, "profiles": true}
for _, entry := range entries {
if !entry.IsDir() || skip[entry.Name()] {
continue
}
info, err := pm.profileInfo(entry.Name())
if err != nil {
continue
}
if _, err := os.Stat(filepath.Join(pm.baseDir, entry.Name(), "Default")); err != nil {
continue
}
isTemporary := strings.HasPrefix(info.Name, "instance-")
pathExists := true
if _, err := os.Stat(info.Path); err != nil {
pathExists = false
}
profiles = append(profiles, bridge.ProfileInfo{
ID: info.ID,
Name: info.Name,
Path: info.Path,
PathExists: pathExists,
Created: info.CreatedAt,
Temporary: isTemporary,
DiskUsage: int64(info.SizeMB * 1024 * 1024),
Source: info.Source,
ChromeProfileName: info.ChromeProfileName,
AccountEmail: info.AccountEmail,
AccountName: info.AccountName,
HasAccount: info.HasAccount,
UseWhen: info.UseWhen,
Description: info.Description,
})
}
sort.Slice(profiles, func(i, j int) bool { return profiles[i].Name < profiles[j].Name })
return profiles, nil
}
func (pm *ProfileManager) profileInfo(dirName string) (ProfileDetailedInfo, error) {
if err := ValidateProfileName(dirName); err != nil {
return ProfileDetailedInfo{}, err
}
dir := filepath.Join(pm.baseDir, dirName)
fi, err := os.Stat(dir)
if err != nil {
return ProfileDetailedInfo{}, err
}
size := dirSizeMB(dir)
source := "created"
if _, err := os.Stat(filepath.Join(dir, ".pinchtab-imported")); err == nil {
source = "imported"
}
chromeProfileName, accountEmail, accountName, hasAccount := readChromeProfileIdentity(dir)
meta := readProfileMeta(dir)
profileName := meta.Name
if profileName == "" {
profileName = dirName
}
changed := false
if meta.ID == "" {
meta.ID = profileID(profileName)
changed = true
}
if meta.Name == "" {
meta.Name = profileName
changed = true
}
if changed {
_ = writeProfileMeta(dir, meta)
}
return ProfileDetailedInfo{
ID: meta.ID,
Name: profileName,
Path: dir,
CreatedAt: fi.ModTime(),
SizeMB: size,
Source: source,
ChromeProfileName: chromeProfileName,
AccountEmail: accountEmail,
AccountName: accountName,
HasAccount: hasAccount,
UseWhen: meta.UseWhen,
Description: meta.Description,
}, nil
}
func (pm *ProfileManager) Import(name, sourcePath string) error {
if err := ValidateProfileName(name); err != nil {
return err
}
pm.mu.Lock()
defer pm.mu.Unlock()
if _, err := pm.findProfileDirByName(name); err == nil {
return fmt.Errorf("profile %q already exists", name)
}
dest := filepath.Join(pm.baseDir, profileID(name))
if _, err := os.Stat(dest); err == nil {
return fmt.Errorf("profile %q already exists", name)
}
if _, err := os.Stat(filepath.Join(sourcePath, "Default")); err != nil {
if _, err2 := os.Stat(filepath.Join(sourcePath, "Preferences")); err2 != nil {
return fmt.Errorf("source doesn't look like a Chrome user data dir (no Default/ or Preferences found)")
}
}
srcInfo, err := os.Stat(sourcePath)
if err != nil {
return fmt.Errorf("source path invalid: %w", err)
}
if !srcInfo.IsDir() {
return fmt.Errorf("source path must be a directory")
}
slog.Info("importing profile", "name", name, "source", sourcePath)
if err := copyDir(sourcePath, dest); err != nil {
return fmt.Errorf("copy failed: %w", err)
}
if err := os.WriteFile(filepath.Join(dest, ".pinchtab-imported"), []byte(sourcePath), 0600); err != nil {
slog.Warn("failed to write import marker", "err", err)
}
return writeProfileMeta(dest, ProfileMeta{
ID: profileID(name),
Name: name,
})
}
func (pm *ProfileManager) ImportWithMeta(name, sourcePath string, meta ProfileMeta) error {
if err := pm.Import(name, sourcePath); err != nil {
return err
}
if meta.ID == "" {
meta.ID = profileID(name)
}
if meta.Name == "" {
meta.Name = name
}
dest := filepath.Join(pm.baseDir, profileID(name))
return writeProfileMeta(dest, meta)
}
func (pm *ProfileManager) Create(name string) error {
if err := ValidateProfileName(name); err != nil {
return err
}
pm.mu.Lock()
defer pm.mu.Unlock()
if _, err := pm.findProfileDirByName(name); err == nil {
return fmt.Errorf("profile %q already exists", name)
}
dest := filepath.Join(pm.baseDir, profileID(name))
if _, err := os.Stat(dest); err == nil {
return fmt.Errorf("profile %q already exists", name)
}
if err := os.MkdirAll(filepath.Join(dest, "Default"), 0755); err != nil {
return err
}
return writeProfileMeta(dest, ProfileMeta{
ID: profileID(name),
Name: name,
})
}
func (pm *ProfileManager) CreateWithMeta(name string, meta ProfileMeta) error {
if err := pm.Create(name); err != nil {
return err
}
if meta.ID == "" {
meta.ID = profileID(name)
}
if meta.Name == "" {
meta.Name = name
}
dest := filepath.Join(pm.baseDir, profileID(name))
return writeProfileMeta(dest, meta)
}
func (pm *ProfileManager) Reset(name string) error {
if err := ValidateProfileName(name); err != nil {
return err
}
pm.mu.Lock()
defer pm.mu.Unlock()
dir, err := pm.findProfileDirByName(name)
if err != nil {
return err
}
nukeDirs := []string{
"Default/Sessions",
"Default/Session Storage",
"Default/Cache",
"Default/Code Cache",
"Default/GPUCache",
"Default/Service Worker",
"Default/blob_storage",
"ShaderCache",
"GrShaderCache",
}
nukeFiles := []string{
"Default/Cookies",
"Default/Cookies-journal",
"Default/History",
"Default/History-journal",
"Default/Visited Links",
}
for _, d := range nukeDirs {
path := filepath.Join(dir, d)
if err := os.RemoveAll(path); err != nil {
slog.Warn("reset: failed to remove dir", "path", path, "err", err)
}
}
for _, f := range nukeFiles {
_ = os.Remove(filepath.Join(dir, f))
}
slog.Info("profile reset", "name", name)
return nil
}
func (pm *ProfileManager) Delete(name string) error {
if err := ValidateProfileName(name); err != nil {
return err
}
pm.mu.Lock()
defer pm.mu.Unlock()
dir, err := pm.findProfileDirByName(name)
if err != nil {
return err
}
return os.RemoveAll(dir)
}
func (pm *ProfileManager) RecordAction(profile string, record bridge.ActionRecord) {
pm.tracker.Record(profile, record)
}
func (pm *ProfileManager) Logs(name string, limit int) []bridge.ActionRecord {
return pm.tracker.GetLogs(name, limit)
}
func (pm *ProfileManager) Analytics(name string) bridge.AnalyticsReport {
return pm.tracker.Analyze(name)
}
func dirSizeMB(path string) float64 {
var total int64
_ = filepath.WalkDir(path, func(_ string, entry fs.DirEntry, err error) error {
if err != nil || entry.IsDir() {
return nil
}
info, err := entry.Info()
if err == nil {
total += info.Size()
}
return nil
})
return float64(total) / (1024 * 1024)
}
func (pm *ProfileManager) UpdateMeta(name string, meta map[string]string) error {
pm.mu.Lock()
defer pm.mu.Unlock()
if err := ValidateProfileName(name); err != nil {
return err
}
dir, err := pm.findProfileDirByName(name)
if err != nil {
return err
}
existing := readProfileMeta(dir)
if existing.Name == "" {
existing.Name = name
}
if useWhen, ok := meta["useWhen"]; ok {
existing.UseWhen = useWhen
}
if description, ok := meta["description"]; ok {
existing.Description = description
}
return writeProfileMeta(dir, existing)
}
func (pm *ProfileManager) Rename(oldName, newName string) error {
if err := ValidateProfileName(oldName); err != nil {
return err
}
if err := ValidateProfileName(newName); err != nil {
return err
}
if oldName == newName {
return nil
}
pm.mu.Lock()
defer pm.mu.Unlock()
oldDir, err := pm.findProfileDirByName(oldName)
if err != nil {
return err
}
if _, err := pm.findProfileDirByName(newName); err == nil {
return fmt.Errorf("profile %q already exists", newName)
}
newDir := filepath.Join(pm.baseDir, profileID(newName))
if _, err := os.Stat(newDir); err == nil {
return fmt.Errorf("profile directory for %q already exists", newName)
}
meta := readProfileMeta(oldDir)
meta.ID = profileID(newName)
meta.Name = newName
if err := writeProfileMeta(oldDir, meta); err != nil {
return fmt.Errorf("failed to update profile metadata: %w", err)
}
if err := os.Rename(oldDir, newDir); err != nil {
meta.ID = profileID(oldName)
meta.Name = oldName
_ = writeProfileMeta(oldDir, meta)
return fmt.Errorf("failed to rename profile directory: %w", err)
}
slog.Info("profile renamed", "from", oldName, "to", newName)
return nil
}
func (pm *ProfileManager) FindByID(id string) (string, error) {
pm.mu.RLock()
defer pm.mu.RUnlock()
entries, err := os.ReadDir(pm.baseDir)
if err != nil {
return "", err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
dir := filepath.Join(pm.baseDir, entry.Name())
meta := readProfileMeta(dir)
if meta.ID == id {
if meta.Name != "" {
return meta.Name, nil
}
return entry.Name(), nil
}
if entry.Name() == id && meta.Name != "" {
return meta.Name, nil
}
if meta.ID == "" && profileID(entry.Name()) == id {
return entry.Name(), nil
}
}
return "", fmt.Errorf("profile with id %q not found", id)
}