package main import ( "crypto/sha256" "fmt" "io" "io/fs" "log" "os" "strings" ) // Searches the build toolchain output directory for new LineageOS builds. // Any new builds are moved into the public http server directory. // Returns true if new builds were moved func moveBuildArtifacts() bool { f, err := os.Open(buildOutDirectory) if err != nil { log.Printf("failed to open ROM directory: %v", err) return false } defer f.Close() files, err := f.ReadDir(0) if err != nil { log.Printf("failed to read files in directory: %v", err) return false } var newROMs bool for _, v := range files { if isLineageROM, _ := isLineageROMZip(v); !isLineageROM { // skip files that aren't LineageOS ROMs continue } newROMs = true log.Printf("new build found - moving file %s", v.Name()) romCache.Lock() // RW lock to prevent multiple concurrent goroutines moving the same file err := copyThenDeleteFile(fmt.Sprintf("%s/%s", buildOutDirectory, v.Name()), fmt.Sprintf("%s/%s", romDirectory, v.Name())) romCache.Unlock() if err != nil { log.Printf("failed to move file '%s' from out to rom directory: %v", v.Name(), err) continue } } return newROMs } // Returns a sha256 hash of a file located at the path provided func hashFile(filename string) (string, error) { f, err := os.Open(filename) if err != nil { return "", fmt.Errorf("failed to open file '%s': %v: ", filename, err) } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", fmt.Errorf("failed to copy data from file to hash function: %v", err) } return string(h.Sum(nil)), nil } // false if a file is not a LineageOS ROM .zip file // no formal validation is performed - only file naming convention is checked // also returns a lineage ROM's filename sliced and delimited by -'s // Example filename: lineage-20.0-20230604-UNOFFICIAL-sunfish.zip func isLineageROMZip(v fs.DirEntry) (bool, []string) { // skip directories, non .zip files and files that don't begin with lineage- if v.Type().IsDir() || !strings.HasSuffix(v.Name(), ".zip") || !strings.HasPrefix(v.Name(), "lineage-") { return false, nil } splitName := strings.Split(v.Name(), "-") // expect 5 dashes return len(splitName) == 5, splitName } // A custom "move file" function because in docker container the mounted folders are different overlay filesystems // Instead of os.Rename, we must copy and delete func copyThenDeleteFile(src, dst string) error { sourceFileStat, err := os.Stat(src) if err != nil { return err } if !sourceFileStat.Mode().IsRegular() { return fmt.Errorf("%s is not a regular file", src) } source, err := os.Open(src) if err != nil { return err } defer source.Close() destination, err := os.Create(dst) if err != nil { return err } defer destination.Close() _, err = io.Copy(destination, source) if err != nil { return fmt.Errorf("failed to copy file: %v", err) } err = os.Remove(src) if err != nil { return fmt.Errorf("failed to delete source file after copy: %v", err) } return nil }