package redirector import ( "encoding/json" "errors" "io" "net/url" "os" "path" "strings" log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" "golang.org/x/text/cases" "golang.org/x/text/language" ) // ErrUnsupportedFormat is returned when an unsupported map format is used. var ErrUnsupportedFormat = errors.New("unsupported map format") var extensionFormats = []string{".asc", ".sha", ".torrent"} // loadMapFile loads a file as a map func loadMapFile(file string, specialExtensions map[string]string) (map[string]string, error) { f, err := os.Open(file) if err != nil { return nil, err } defer f.Close() ext := path.Ext(file) switch ext { case ".json": return loadMapJSON(f, specialExtensions) } return nil, ErrUnsupportedFormat } // Map represents a JSON format of an asset list type Map struct { Assets []ReleaseFile `json:"assets"` } // ReleaseFile represents a file to be mapped type ReleaseFile struct { BoardSlug string `json:"board_slug"` FileURL string `json:"file_url"` FileURLASC string `json:"file_url_asc"` FileURLSHA string `json:"file_url_sha"` FileURLTorrent string `json:"file_url_torrent"` FileUpdated string `json:"file_updated"` FileSize string `json:"file_size"` DistroRelease string `json:"distro"` KernelBranch string `json:"branch"` ImageVariant string `json:"variant"` Preinstalled string `json:"file_application"` Promoted string `json:"promoted"` Repository string `json:"download_repository"` Extension string `json:"file_extension"` } var distroCaser = cases.Title(language.Und) // loadMapJSON loads a map file from JSON, based on the format specified in the github issue. // See: https://github.com/armbian/os/pull/129 func loadMapJSON(f io.Reader, specialExtensions map[string]string) (map[string]string, error) { // Avoid panics if specialExtensions == nil { specialExtensions = make(map[string]string) } m := make(map[string]string) var data Map if err := json.NewDecoder(f).Decode(&data); err != nil { return nil, err } for _, file := range data.Assets { // Because download mapping a full URL, redirecting, and finding a server again is redundant, // we parse the URL and only return the path here. Previously, it would use https://dl.armbian.com/PATH // which is not supported, as the redirector will always prepend a server u, err := url.Parse(file.FileURL) if err != nil { log.WithFields(log.Fields{ "error": err, "uri": file.FileURL, }).Warning("Error parsing redirect url or path") continue } var sb strings.Builder if file.Repository == "os" { sb.WriteString("nightly/") } sb.WriteString(file.BoardSlug) sb.WriteString("/") sb.WriteString(distroCaser.String(file.DistroRelease)) sb.WriteString("_") sb.WriteString(file.KernelBranch) sb.WriteString("_") sb.WriteString(file.ImageVariant) if file.Preinstalled != "" { sb.WriteString("-") sb.WriteString(file.Preinstalled) } // Check special case for some extensions for k, v := range specialExtensions { if strings.Contains(file.Extension, k) { sb.WriteString(v) break } } imageExtensions := maps.Keys(specialExtensions) imageExtensions = append(imageExtensions, "img.xz") // extra allocation, but it's fine // Add board into the map without an extension for _, ext := range imageExtensions { if strings.HasSuffix(file.Extension, ext) { m[sb.String()] = u.Path break } } // Also add entries for .asc .sha and .torrent for _, ext := range extensionFormats { if strings.HasSuffix(file.Extension, ext) { continue } var filePath string switch ext { case ".asc": filePath = file.FileURLASC case ".sha": filePath = file.FileURLSHA case ".torrent": filePath = file.FileURLTorrent } m[sb.String()+ext] = filePath } sb.WriteString(".") sb.WriteString(file.Extension) m[sb.String()] = u.Path // Add board into the map with an extension } return m, nil }