mccl/internal/assets/assets.go

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
}