package main import ( "archive/zip" "bufio" "crypto/sha1" "crypto/sha256" "fmt" "io" "io/fs" "log" "os" "strconv" "strings" ) type ZipMetadata struct { Timestamp int SDKLevel int PatchLevel string OTAPropertyFiles string } // 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, _ := parseROMFileName(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) { _, sha256Val, err := hashFileSha1AndSha256(filename) return sha256Val, err } // Returns sha1 and sha256 hashes of a file located at the path provided func hashFileSha1AndSha256(filename string) (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() h1 := sha1.New() h256 := sha256.New() buf := make([]byte, 32768) for { n, err := f.Read(buf) if n > 0 { h1.Write(buf[:n]) h256.Write(buf[:n]) } if err == io.EOF { break } if err != nil { return "", "", fmt.Errorf("failed to read file '%s': %v", filename, err) } } return fmt.Sprintf("%x", h1.Sum(nil)), fmt.Sprintf("%x", h256.Sum(nil)), nil } // Reads metadata from META-INF/com/android/metadata inside the zip file func readZipMetadata(zipPath string) (ZipMetadata, error) { var meta ZipMetadata r, err := zip.OpenReader(zipPath) if err != nil { return meta, err } defer r.Close() for _, f := range r.File { if f.Name == "META-INF/com/android/metadata" { rc, err := f.Open() if err != nil { return meta, err } defer rc.Close() scanner := bufio.NewScanner(rc) for scanner.Scan() { line := scanner.Text() parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) switch key { case "post-timestamp": t, _ := strconv.Atoi(val) meta.Timestamp = t case "post-sdk-level": s, _ := strconv.Atoi(val) meta.SDKLevel = s case "security-patch-level": meta.PatchLevel = val case "ota-property-files": meta.OTAPropertyFiles = val } } } return meta, scanner.Err() } } return meta, fmt.Errorf("metadata file not found in zip") } // 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 parseROMFileName(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 }