package steam import ( "archive/tar" "errors" "fmt" "io" "net/url" "os" "path/filepath" "strings" "time" ) // how often are we going to be updating our status information? const updateEveryNBytes = 10 * 1024 * 1024 // 10mb // ExtractSmart attempts to discover what kind of resource is behind uri // and extract it appropriately. It may fail with E_BadURI. // // For example the following forms are accepted: // // ExtractSmart("http://127.0.0.1/some-archive") // ExtractSmart("https://example.com/some-archive") // ExtractSmart("file:///some/local/file/path/to/archive.tar") // ExtractSmart("/direct/path/to/archive.tar") // ExtractSmart("C:\Users\user\Downloads\archive.tar") func (l *Library) ExtractSmart(uri string) (*Game, error) { if strings.HasPrefix(uri, "http") { _, err := url.Parse(uri) if err == nil { return l.ExtractHTTP(uri) } } else if strings.HasPrefix(uri, "file") { u, err := url.Parse(uri) if err == nil { return l.ExtractFile(u.Path) } } else if _, err := os.Stat(uri); err == nil { return l.ExtractFile(uri) } return nil, E_BadURI } // ExtractFile is a wrapper around Extract that handles local files. this // spawns an "extractFile" on the library. Status will be updated there as this // goes along. Non fatal and fatal errors will be populated there func (l *Library) ExtractFile(fn string) (*Game, error) { g := &Game{} j := newJob("extractFile", g) defer j.done() l.status.addJob(j) fi, err := os.Stat(fn) if err != nil { j.addError(err) return g, err } j.setSize(fi.Size()) fh, err := os.Open(fn) if err != nil { j.addError(err) return g, err } g, err = l.extractUpdate(j, g, fh) fh.Close() return g, err } // Extract will read a tarball from the io.ReadCloser and install the game into // the current library path. This offers no visibility into the progress, // as it does not update the job status on the progress, though it will // populate errors. // // Most callers will want to use ExtractHTTP or ExtractFile instead func (l *Library) Extract(r io.Reader) (*Game, error) { g := &Game{LibraryPath: l.folder} j := newJob("extract", g) defer j.done() l.status.addJob(j) return l.extractPrimitive(j, g, r) } // extractUpdate takes care of updating the job as it goes along at updateEveryNBytes // it will be reported back to the Job's status. func (l *Library) extractUpdate(j *Job, g *Game, rdr io.Reader) (*Game, error) { prdr, pwrtr := io.Pipe() go func() { var err error g, err = l.extractPrimitive(j, g, prdr) if err != nil { j.addError(fmt.Errorf("Installer: extracting %s", err)) } }() var total int64 var err error var n int64 for { n, err = io.CopyN(pwrtr, rdr, updateEveryNBytes) if err == io.EOF { break } else if err != nil { j.addError(fmt.Errorf( "Error encountered read error: %w", err)) break } total += n j.setTransferred(total) // rate in bytes/sec // rate := total / int64(time.Since(*j.StartTime()).Seconds()) rate := float64(total) / float64(time.Since(*j.StartTime()).Seconds()) estSize := j.GetSize() if estSize == nil { j.addError(errors.New("Expected an estimated size, got nil")) continue } // remaining := *estSize - total remaining := float64(*estSize - total) j.setETA(time.Duration((remaining / rate) / 1000 / 1000 / 1000)) } if err == io.EOF { return g, nil } return g, err } func (l *Library) extractPrimitive(j *Job, g *Game, r io.Reader) (*Game, error) { treader := tar.NewReader(r) for { hdr, err := treader.Next() if err == io.EOF { // We've reached the end! Whoee break } if err != nil { err = fmt.Errorf("tar reader: %w", err) j.addError(err) return nil, err } fileName := filepath.ToSlash(hdr.Name) if g.Name == "" { s := strings.Split(fileName, "/") if len(s) >= 2 { g.Name = s[1] } } fileName = filepath.Join(l.folder, fileName) info := hdr.FileInfo() if info.IsDir() { err = os.MkdirAll(fileName, defaultDirectoryMode) if err != nil { err = fmt.Errorf("os.MkDirAll for directory: %w", err) j.addError(err) return nil, err } continue } err = os.MkdirAll(filepath.Dir(fileName), defaultDirectoryMode) if err != nil { err = fmt.Errorf("os.MkDirAll for file: %w", err) j.addError(err) return nil, err } // Create a file handle to work with f, err := os.OpenFile(fileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFileMode) if err != nil { err = fmt.Errorf( "while opening file handle for tarball extraction: %w", err) j.addError(err) return nil, err } if _, err := io.Copy(f, treader); err != nil { err = fmt.Errorf("while copying from treader: %w", err) j.addError(err) f.Close() return nil, err } f.Close() } if g.LibraryPath == "" { g.LibraryPath = l.folder } err := g.SetSizeInfo() if err != nil { err = fmt.Errorf("while setting size info: %w", err) j.addError(err) return nil, err } l.m.Lock() l.games[g.Name] = g l.m.Unlock() return g, nil }