package assets import ( "crypto/sha1" "encoding/hex" "encoding/json" "errors" "fmt" "io" "mccl/pkg/retriever" "net/http" "os" "path" ) // assetDownloadUrl is a base URL for an asset to be retrieved from, first %s // is a directory which name is first two characters of a hash that goes // under the second %s. const assetDownloadUrl = "https://resources.download.minecraft.net/%s/%s" // Asset represents a single asset file, that is some resource like a texture, // or a sound, and so on. type Asset struct { Hash string `json:"hash"` Size int64 `json:"size"` } // url forms a full URL to an asset. func (a *Asset) url() string { return fmt.Sprintf(assetDownloadUrl, a.Hash[:2], a.Hash) } // path forms a relative path to an asset. func (a *Asset) path() string { return path.Join(a.Hash[:2], a.Hash) } // Retrieve is a method of a Retrievable interface that is used to download // an asset from Mojang's server. It performs the checks for a valid size // and hash. Returns a slice of bytes an asset consists of or an error // if something gone wrong. func (a Asset) Retrieve() ([]byte, error) { resp, err := http.Get(a.url()) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to download a file. Status code is %d", resp.StatusCode) } else if resp.ContentLength != a.Size { return nil, fmt.Errorf("response size mismatch. %d != %d", resp.ContentLength, a.Size) } buf, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if l := len(buf); l != int(a.Size) { return nil, fmt.Errorf("file size mismatch. %d != %d", l, a.Size) } h := sha1.New() if _, err := h.Write(buf); err != nil { return nil, err } if hex.EncodeToString(h.Sum(nil)) != a.Hash { return nil, errors.New("hash mismatch") } return buf, nil } // Assets represents an asset index file. type Assets struct { Objects map[string]Asset `json:"objects"` } // New returns a new Assets that is being unmarshaled from a JSON data. func New(data []byte) (*Assets, error) { a := &Assets{} if err := json.Unmarshal(data, a); err != nil { return nil, err } return a, nil } // RetrieveAsset is a special retriever.WorkerFunc function that is used to // check for an existency of an asset, and, if not, to retrieve it and write // it to an actual file. func RetrieveAsset(item retriever.Retrievable, basePath string) (int, error) { asset := item.(Asset) p := path.Join(basePath, asset.path()) if s, err := os.Stat(p); err == nil && s.Size() == asset.Size { return int(s.Size()), nil } data, err := item.Retrieve() if err != nil { return 0, err } if err := os.MkdirAll(path.Dir(p), 0777); err != nil { return 0, err } tf, err := os.Create(p) if err != nil { return 0, err } defer tf.Close() written, err := tf.Write(data) if err != nil { return 0, err } else if written != len(data) { return 0, fmt.Errorf("written bytes length mismatch. %d != %d", written, len(data)) } return written, nil }