// Package steam is designed to be a rather simplistic library to help pull // information from a local steam library and run basic operations like // archiving, restore and deleting games package steam import ( "errors" "fmt" "os" "regexp" "riedstra.dev/mitch/steam-export/tasks" "sync" "github.com/barkimedes/go-deepcopy" ) var ( E_GameDoesNotExist = errors.New("game does not exist") E_BadURI = errors.New("the URI supplied is not understood") E_OperationConflict = errors.New("another conflicting job is running on this game right now") E_NoEstimatedSize = errors.New("expected an estimated size, got nil") E_LibraryLocked = errors.New("cannot process library with actions running, library is locked") E_LibraryNoCommon = errors.New("no common directory") ) // Library is used to represent the steam library, the Games map is populated // by NewLibrary when called or when ProcessLibrary is called directly // The key in the map is the same as the Game's `Name` property. // // Status contains various bits of information about previous jobs // and the status of any currently running jobs type Library struct { folder string games map[string]*Game status *tasks.Group m sync.Mutex } var slugregexp = regexp.MustCompile(`[^-0-9A-Za-z_:.]`) // Slug returns a safer version of the name with spaces and other chars // transformed into dashes for use in HTML element ids and such. func (g Game) Slug() string { return slugregexp.ReplaceAllString(g.Name, "-") } // NewLibrary returns a pointer to a processed library and an error // if any func NewLibrary(path string) (*Library, error) { l := &Library{ status: tasks.NewGroup(), } err := l.ProcessLibrary(path) if err != nil { return nil, err } return l, err } // NewLibraryMust is the same as NewLibrary but calls panic if there // is any error func NewLibraryMust(path string) *Library { l, err := NewLibrary(path) if err != nil { panic(err) } return l } // Folder returns the current folder on the disk that contains the steam library func (l *Library) Folder() string { l.m.Lock() defer l.m.Unlock() return l.folder } // Games returns a map of string[*Game] for the current library func (l *Library) Games() map[string]*Game { l.m.Lock() defer l.m.Unlock() g := deepcopy.MustAnything(l.games) return g.(map[string]*Game) } // Status returns the current Jobs struct which can be used to keep track // of any long-running operations on the library as well as any errors // encountered along the way func (l *Library) Status() *tasks.Group { l.m.Lock() defer l.m.Unlock() s2 := deepcopy.MustAnything(l.status) return s2.(*tasks.Group) } // Refresh simply calls ProcessLibrary to refresh the entire contents of the // steam library. Will return an error if any jobs are running func (l *Library) Refresh() error { return l.ProcessLibrary(l.folder) } // ProcessLibrary Populates the "Folder" and "Games" fields based on the // provided directory. Returns an error if any jobs are currently running func (s *Library) ProcessLibrary(r string) error { if len(s.status.Running()) > 0 { return E_LibraryLocked } if !hasCommon(r) { return fmt.Errorf("in: '%s': %w", r, E_LibraryNoCommon) } s.m.Lock() defer s.m.Unlock() s.games = make(map[string]*Game) dirs, err := os.ReadDir(r + "/common") if err != nil { return err } s.folder = r for _, f := range dirs { if f.IsDir() { g := &Game{ Name: f.Name(), LibraryPath: r, } _ = g.SetSizeInfo() s.games[f.Name()] = g } } return nil } func (s *Library) String() (str string) { str = fmt.Sprintf("Library: %s\n", s.folder) str = str + "----\n" for _, v := range s.games { str = str + fmt.Sprintf("%s\n", v.Name) } return } func hasCommon(d string) bool { dirs, err := os.ReadDir(d) if err != nil { return false } for _, f := range dirs { if f.Name() == "common" && f.IsDir() { return true } } return false }