125 lines
3.1 KiB
Go
Executable File
125 lines
3.1 KiB
Go
Executable File
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
|
|
}
|