// 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" "io/ioutil" "regexp" "sync" ) 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") ) // 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 *Jobs m sync.Mutex } // Game represents an actual game in the steam Library. The purpose is only // to provide info on a game. type Game struct { Name string LibraryPath string Size int64 } 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: &Jobs{ running: make([]*Job, 0), previous: make([]*Job, 0), }, } 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() return l.games } // Jobs 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() Jobs { l.m.Lock() defer l.m.Unlock() return *l.status } // 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 s.status.Running() { return errors.New("Cannot process library with actions running") } if !hasCommon(r) { return fmt.Errorf("No common directory in: %s", r) } s.m.Lock() defer s.m.Unlock() s.games = make(map[string]*Game) dirs, err := ioutil.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 := ioutil.ReadDir(d) if err != nil { return false } for _, f := range dirs { if f.Name() == "common" && f.IsDir() { return true } } return false }