github.com/linapex/ethereum-dpos-chinese@v0.0.0-20190316121959-b78b3a4a1ece/swarm/api/client/client.go (about)

     1  
     2  //<developer>
     3  //    <name>linapex 曹一峰</name>
     4  //    <email>linapex@163.com</email>
     5  //    <wx>superexc</wx>
     6  //    <qqgroup>128148617</qqgroup>
     7  //    <url>https://jsq.ink</url>
     8  //    <role>pku engineer</role>
     9  //    <date>2019-03-16 12:09:46</date>
    10  //</624342668566073344>
    11  
    12  
    13  package client
    14  
    15  import (
    16  	"archive/tar"
    17  	"bytes"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"mime"
    24  	"mime/multipart"
    25  	"net/http"
    26  	"net/textproto"
    27  	"os"
    28  	"path/filepath"
    29  	"regexp"
    30  	"strconv"
    31  	"strings"
    32  
    33  	"github.com/ethereum/go-ethereum/swarm/api"
    34  	"github.com/ethereum/go-ethereum/swarm/storage/mru"
    35  )
    36  
    37  var (
    38  DefaultGateway = "http://本地主机:8500
    39  	DefaultClient  = NewClient(DefaultGateway)
    40  )
    41  
    42  var (
    43  	ErrUnauthorized = errors.New("unauthorized")
    44  )
    45  
    46  func NewClient(gateway string) *Client {
    47  	return &Client{
    48  		Gateway: gateway,
    49  	}
    50  }
    51  
    52  //客户端将与Swarm HTTP网关的交互进行包装。
    53  type Client struct {
    54  	Gateway string
    55  }
    56  
    57  //uploadraw将原始数据上载到swarm并返回结果哈希。如果加密是真的
    58  //上载加密数据
    59  func (c *Client) UploadRaw(r io.Reader, size int64, toEncrypt bool) (string, error) {
    60  	if size <= 0 {
    61  		return "", errors.New("data size must be greater than zero")
    62  	}
    63  	addr := ""
    64  	if toEncrypt {
    65  		addr = "encrypt"
    66  	}
    67  	req, err := http.NewRequest("POST", c.Gateway+"/bzz-raw:/"+addr, r)
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	req.ContentLength = size
    72  	res, err := http.DefaultClient.Do(req)
    73  	if err != nil {
    74  		return "", err
    75  	}
    76  	defer res.Body.Close()
    77  	if res.StatusCode != http.StatusOK {
    78  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
    79  	}
    80  	data, err := ioutil.ReadAll(res.Body)
    81  	if err != nil {
    82  		return "", err
    83  	}
    84  	return string(data), nil
    85  }
    86  
    87  //downloadraw从swarm下载原始数据,它返回readcloser和bool
    88  //内容已加密
    89  func (c *Client) DownloadRaw(hash string) (io.ReadCloser, bool, error) {
    90  	uri := c.Gateway + "/bzz-raw:/" + hash
    91  	res, err := http.DefaultClient.Get(uri)
    92  	if err != nil {
    93  		return nil, false, err
    94  	}
    95  	if res.StatusCode != http.StatusOK {
    96  		res.Body.Close()
    97  		return nil, false, fmt.Errorf("unexpected HTTP status: %s", res.Status)
    98  	}
    99  	isEncrypted := (res.Header.Get("X-Decrypted") == "true")
   100  	return res.Body, isEncrypted, nil
   101  }
   102  
   103  //文件表示群清单中的文件,用于上传和
   104  //从Swarm下载内容
   105  type File struct {
   106  	io.ReadCloser
   107  	api.ManifestEntry
   108  }
   109  
   110  //打开打开一个本地文件,然后可以将其传递到客户端。上载以上载
   111  //它蜂拥而至
   112  func Open(path string) (*File, error) {
   113  	f, err := os.Open(path)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	stat, err := f.Stat()
   118  	if err != nil {
   119  		f.Close()
   120  		return nil, err
   121  	}
   122  	return &File{
   123  		ReadCloser: f,
   124  		ManifestEntry: api.ManifestEntry{
   125  			ContentType: mime.TypeByExtension(filepath.Ext(path)),
   126  			Mode:        int64(stat.Mode()),
   127  			Size:        stat.Size(),
   128  			ModTime:     stat.ModTime(),
   129  		},
   130  	}, nil
   131  }
   132  
   133  //上载将文件上载到Swarm,并将其添加到现有清单中
   134  //(如果manifest参数非空)或创建包含
   135  //文件,返回生成的清单哈希(然后该文件将
   136  //可在bzz:/<hash>/<path>)获取
   137  func (c *Client) Upload(file *File, manifest string, toEncrypt bool) (string, error) {
   138  	if file.Size <= 0 {
   139  		return "", errors.New("file size must be greater than zero")
   140  	}
   141  	return c.TarUpload(manifest, &FileUploader{file}, "", toEncrypt)
   142  }
   143  
   144  //下载从swarm manifest下载具有给定路径的文件
   145  //给定的哈希(即它得到bzz:/<hash>/<path>)
   146  func (c *Client) Download(hash, path string) (*File, error) {
   147  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   148  	res, err := http.DefaultClient.Get(uri)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	if res.StatusCode != http.StatusOK {
   153  		res.Body.Close()
   154  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   155  	}
   156  	return &File{
   157  		ReadCloser: res.Body,
   158  		ManifestEntry: api.ManifestEntry{
   159  			ContentType: res.Header.Get("Content-Type"),
   160  			Size:        res.ContentLength,
   161  		},
   162  	}, nil
   163  }
   164  
   165  //uploadDirectory将目录树上载到swarm并添加文件
   166  //到现有清单(如果清单参数非空)或创建
   167  //新清单,返回生成的清单哈希(来自
   168  //目录将在bzz:/<hash>/path/to/file处可用,其中
   169  //默认路径中指定的文件正在上载到清单的根目录
   170  //(即bzz/<hash>/)
   171  func (c *Client) UploadDirectory(dir, defaultPath, manifest string, toEncrypt bool) (string, error) {
   172  	stat, err := os.Stat(dir)
   173  	if err != nil {
   174  		return "", err
   175  	} else if !stat.IsDir() {
   176  		return "", fmt.Errorf("not a directory: %s", dir)
   177  	}
   178  	if defaultPath != "" {
   179  		if _, err := os.Stat(filepath.Join(dir, defaultPath)); err != nil {
   180  			if os.IsNotExist(err) {
   181  				return "", fmt.Errorf("the default path %q was not found in the upload directory %q", defaultPath, dir)
   182  			}
   183  			return "", fmt.Errorf("default path: %v", err)
   184  		}
   185  	}
   186  	return c.TarUpload(manifest, &DirectoryUploader{dir}, defaultPath, toEncrypt)
   187  }
   188  
   189  //下载目录下载群清单中包含的文件
   190  //到本地目录的给定路径(现有文件将被覆盖)
   191  func (c *Client) DownloadDirectory(hash, path, destDir, credentials string) error {
   192  	stat, err := os.Stat(destDir)
   193  	if err != nil {
   194  		return err
   195  	} else if !stat.IsDir() {
   196  		return fmt.Errorf("not a directory: %s", destDir)
   197  	}
   198  
   199  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   200  	req, err := http.NewRequest("GET", uri, nil)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	if credentials != "" {
   205  		req.SetBasicAuth("", credentials)
   206  	}
   207  	req.Header.Set("Accept", "application/x-tar")
   208  	res, err := http.DefaultClient.Do(req)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	defer res.Body.Close()
   213  	switch res.StatusCode {
   214  	case http.StatusOK:
   215  	case http.StatusUnauthorized:
   216  		return ErrUnauthorized
   217  	default:
   218  		return fmt.Errorf("unexpected HTTP status: %s", res.Status)
   219  	}
   220  	tr := tar.NewReader(res.Body)
   221  	for {
   222  		hdr, err := tr.Next()
   223  		if err == io.EOF {
   224  			return nil
   225  		} else if err != nil {
   226  			return err
   227  		}
   228  //忽略默认路径文件
   229  		if hdr.Name == "" {
   230  			continue
   231  		}
   232  
   233  		dstPath := filepath.Join(destDir, filepath.Clean(strings.TrimPrefix(hdr.Name, path)))
   234  		if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
   235  			return err
   236  		}
   237  		var mode os.FileMode = 0644
   238  		if hdr.Mode > 0 {
   239  			mode = os.FileMode(hdr.Mode)
   240  		}
   241  		dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   242  		if err != nil {
   243  			return err
   244  		}
   245  		n, err := io.Copy(dst, tr)
   246  		dst.Close()
   247  		if err != nil {
   248  			return err
   249  		} else if n != hdr.Size {
   250  			return fmt.Errorf("expected %s to be %d bytes but got %d", hdr.Name, hdr.Size, n)
   251  		}
   252  	}
   253  }
   254  
   255  //下载文件将单个文件下载到目标目录中
   256  //如果清单项未指定文件名-它将回退
   257  //以文件名的形式传递到文件的哈希
   258  func (c *Client) DownloadFile(hash, path, dest, credentials string) error {
   259  	hasDestinationFilename := false
   260  	if stat, err := os.Stat(dest); err == nil {
   261  		hasDestinationFilename = !stat.IsDir()
   262  	} else {
   263  		if os.IsNotExist(err) {
   264  //不存在-应创建
   265  			hasDestinationFilename = true
   266  		} else {
   267  			return fmt.Errorf("could not stat path: %v", err)
   268  		}
   269  	}
   270  
   271  	manifestList, err := c.List(hash, path, credentials)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	switch len(manifestList.Entries) {
   277  	case 0:
   278  		return fmt.Errorf("could not find path requested at manifest address. make sure the path you've specified is correct")
   279  	case 1:
   280  //持续
   281  	default:
   282  		return fmt.Errorf("got too many matches for this path")
   283  	}
   284  
   285  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   286  	req, err := http.NewRequest("GET", uri, nil)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	if credentials != "" {
   291  		req.SetBasicAuth("", credentials)
   292  	}
   293  	res, err := http.DefaultClient.Do(req)
   294  	if err != nil {
   295  		return err
   296  	}
   297  	defer res.Body.Close()
   298  	switch res.StatusCode {
   299  	case http.StatusOK:
   300  	case http.StatusUnauthorized:
   301  		return ErrUnauthorized
   302  	default:
   303  		return fmt.Errorf("unexpected HTTP status: expected 200 OK, got %d", res.StatusCode)
   304  	}
   305  	filename := ""
   306  	if hasDestinationFilename {
   307  		filename = dest
   308  	} else {
   309  //尝试断言
   310  re := regexp.MustCompile("[^/]+$") //最后一个斜线后的所有内容
   311  
   312  		if results := re.FindAllString(path, -1); len(results) > 0 {
   313  			filename = results[len(results)-1]
   314  		} else {
   315  			if entry := manifestList.Entries[0]; entry.Path != "" && entry.Path != "/" {
   316  				filename = entry.Path
   317  			} else {
   318  //如果命令行中没有任何内容,则假定hash为名称
   319  				filename = hash
   320  			}
   321  		}
   322  		filename = filepath.Join(dest, filename)
   323  	}
   324  	filePath, err := filepath.Abs(filename)
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	if err := os.MkdirAll(filepath.Dir(filePath), 0777); err != nil {
   330  		return err
   331  	}
   332  
   333  	dst, err := os.Create(filename)
   334  	if err != nil {
   335  		return err
   336  	}
   337  	defer dst.Close()
   338  
   339  	_, err = io.Copy(dst, res.Body)
   340  	return err
   341  }
   342  
   343  //上载清单将给定清单上载到Swarm
   344  func (c *Client) UploadManifest(m *api.Manifest, toEncrypt bool) (string, error) {
   345  	data, err := json.Marshal(m)
   346  	if err != nil {
   347  		return "", err
   348  	}
   349  	return c.UploadRaw(bytes.NewReader(data), int64(len(data)), toEncrypt)
   350  }
   351  
   352  //下载清单下载群清单
   353  func (c *Client) DownloadManifest(hash string) (*api.Manifest, bool, error) {
   354  	res, isEncrypted, err := c.DownloadRaw(hash)
   355  	if err != nil {
   356  		return nil, isEncrypted, err
   357  	}
   358  	defer res.Close()
   359  	var manifest api.Manifest
   360  	if err := json.NewDecoder(res).Decode(&manifest); err != nil {
   361  		return nil, isEncrypted, err
   362  	}
   363  	return &manifest, isEncrypted, nil
   364  }
   365  
   366  //列出具有给定前缀、分组的群清单中的列表文件
   367  //使用“/”作为分隔符的常见前缀。
   368  //
   369  //例如,如果清单表示以下目录结构:
   370  //
   371  //文件1.TXT
   372  //文件2.TXT
   373  //DRI1/FIL3.TXT
   374  //dir1/dir2/file4.txt文件
   375  //
   376  //然后:
   377  //
   378  //-前缀“”将返回[dir1/,file1.txt,file2.txt]
   379  //-前缀“file”将返回[file1.txt,file2.txt]
   380  //-前缀“dir1/”将返回[dir1/dir2/,dir1/file3.txt]
   381  //
   382  //其中以“/”结尾的条目是常见的前缀。
   383  func (c *Client) List(hash, prefix, credentials string) (*api.ManifestList, error) {
   384  	req, err := http.NewRequest(http.MethodGet, c.Gateway+"/bzz-list:/"+hash+"/"+prefix, nil)
   385  	if err != nil {
   386  		return nil, err
   387  	}
   388  	if credentials != "" {
   389  		req.SetBasicAuth("", credentials)
   390  	}
   391  	res, err := http.DefaultClient.Do(req)
   392  	if err != nil {
   393  		return nil, err
   394  	}
   395  	defer res.Body.Close()
   396  	switch res.StatusCode {
   397  	case http.StatusOK:
   398  	case http.StatusUnauthorized:
   399  		return nil, ErrUnauthorized
   400  	default:
   401  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   402  	}
   403  	var list api.ManifestList
   404  	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
   405  		return nil, err
   406  	}
   407  	return &list, nil
   408  }
   409  
   410  //上载程序使用提供的上载将文件上载到Swarm fn
   411  type Uploader interface {
   412  	Upload(UploadFn) error
   413  }
   414  
   415  type UploaderFunc func(UploadFn) error
   416  
   417  func (u UploaderFunc) Upload(upload UploadFn) error {
   418  	return u(upload)
   419  }
   420  
   421  //DirectoryUploader上载目录中的所有文件,可以选择上载
   422  //默认路径的文件
   423  type DirectoryUploader struct {
   424  	Dir string
   425  }
   426  
   427  //上载执行目录和默认路径的上载
   428  func (d *DirectoryUploader) Upload(upload UploadFn) error {
   429  	return filepath.Walk(d.Dir, func(path string, f os.FileInfo, err error) error {
   430  		if err != nil {
   431  			return err
   432  		}
   433  		if f.IsDir() {
   434  			return nil
   435  		}
   436  		file, err := Open(path)
   437  		if err != nil {
   438  			return err
   439  		}
   440  		relPath, err := filepath.Rel(d.Dir, path)
   441  		if err != nil {
   442  			return err
   443  		}
   444  		file.Path = filepath.ToSlash(relPath)
   445  		return upload(file)
   446  	})
   447  }
   448  
   449  //文件上载程序上载单个文件
   450  type FileUploader struct {
   451  	File *File
   452  }
   453  
   454  //上载执行文件上载
   455  func (f *FileUploader) Upload(upload UploadFn) error {
   456  	return upload(f.File)
   457  }
   458  
   459  //uploadfn是传递给上载程序以执行上载的函数类型。
   460  //对于单个文件(例如,目录上载程序将调用
   461  //目录树中每个文件的uploadfn)
   462  type UploadFn func(file *File) error
   463  
   464  //tar upload使用给定的上传器将文件作为tar流上传到swarm,
   465  //返回结果清单哈希
   466  func (c *Client) TarUpload(hash string, uploader Uploader, defaultPath string, toEncrypt bool) (string, error) {
   467  	reqR, reqW := io.Pipe()
   468  	defer reqR.Close()
   469  	addr := hash
   470  
   471  //如果已经存在哈希(清单),那么该清单将确定上载是否
   472  //是否加密。如果没有清单,则toEncrypt参数决定
   473  //是否加密。
   474  	if hash == "" && toEncrypt {
   475  //这是加密上载端点的内置地址
   476  		addr = "encrypt"
   477  	}
   478  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+addr, reqR)
   479  	if err != nil {
   480  		return "", err
   481  	}
   482  	req.Header.Set("Content-Type", "application/x-tar")
   483  	if defaultPath != "" {
   484  		q := req.URL.Query()
   485  		q.Set("defaultpath", defaultPath)
   486  		req.URL.RawQuery = q.Encode()
   487  	}
   488  
   489  //使用“expect:100 continue”,以便在以下情况下不发送请求正文:
   490  //服务器拒绝请求
   491  	req.Header.Set("Expect", "100-continue")
   492  
   493  	tw := tar.NewWriter(reqW)
   494  
   495  //定义将文件添加到tar流的uploadfn
   496  	uploadFn := func(file *File) error {
   497  		hdr := &tar.Header{
   498  			Name:    file.Path,
   499  			Mode:    file.Mode,
   500  			Size:    file.Size,
   501  			ModTime: file.ModTime,
   502  			Xattrs: map[string]string{
   503  				"user.swarm.content-type": file.ContentType,
   504  			},
   505  		}
   506  		if err := tw.WriteHeader(hdr); err != nil {
   507  			return err
   508  		}
   509  		_, err = io.Copy(tw, file)
   510  		return err
   511  	}
   512  
   513  //在Goroutine中运行上载,以便我们可以发送请求头和
   514  //在发送tar流之前,等待“100 continue”响应
   515  	go func() {
   516  		err := uploader.Upload(uploadFn)
   517  		if err == nil {
   518  			err = tw.Close()
   519  		}
   520  		reqW.CloseWithError(err)
   521  	}()
   522  
   523  	res, err := http.DefaultClient.Do(req)
   524  	if err != nil {
   525  		return "", err
   526  	}
   527  	defer res.Body.Close()
   528  	if res.StatusCode != http.StatusOK {
   529  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   530  	}
   531  	data, err := ioutil.ReadAll(res.Body)
   532  	if err != nil {
   533  		return "", err
   534  	}
   535  	return string(data), nil
   536  }
   537  
   538  //multipartupload使用给定的上载程序将文件作为
   539  //多部分表单,返回结果清单哈希
   540  func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error) {
   541  	reqR, reqW := io.Pipe()
   542  	defer reqR.Close()
   543  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
   544  	if err != nil {
   545  		return "", err
   546  	}
   547  
   548  //使用“expect:100 continue”,以便在以下情况下不发送请求正文:
   549  //服务器拒绝请求
   550  	req.Header.Set("Expect", "100-continue")
   551  
   552  	mw := multipart.NewWriter(reqW)
   553  	req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", mw.Boundary()))
   554  
   555  //定义将文件添加到多部分表单的uploadfn
   556  	uploadFn := func(file *File) error {
   557  		hdr := make(textproto.MIMEHeader)
   558  		hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=%q", file.Path))
   559  		hdr.Set("Content-Type", file.ContentType)
   560  		hdr.Set("Content-Length", strconv.FormatInt(file.Size, 10))
   561  		w, err := mw.CreatePart(hdr)
   562  		if err != nil {
   563  			return err
   564  		}
   565  		_, err = io.Copy(w, file)
   566  		return err
   567  	}
   568  
   569  //在Goroutine中运行上载,以便我们可以发送请求头和
   570  //在发送多部分表单之前,请等待“100继续”响应
   571  	go func() {
   572  		err := uploader.Upload(uploadFn)
   573  		if err == nil {
   574  			err = mw.Close()
   575  		}
   576  		reqW.CloseWithError(err)
   577  	}()
   578  
   579  	res, err := http.DefaultClient.Do(req)
   580  	if err != nil {
   581  		return "", err
   582  	}
   583  	defer res.Body.Close()
   584  	if res.StatusCode != http.StatusOK {
   585  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   586  	}
   587  	data, err := ioutil.ReadAll(res.Body)
   588  	if err != nil {
   589  		return "", err
   590  	}
   591  	return string(data), nil
   592  }
   593  
   594  //createResource创建具有给定名称和频率的可变资源,并使用提供的
   595  //数据。数据是否解释为多哈希,取决于多哈希参数。
   596  //starttime=0表示“现在”
   597  //返回生成的可变资源清单地址,可用于包含在ENS解析程序(setcontent)中。
   598  //或引用将来的更新(client.updateResource)
   599  func (c *Client) CreateResource(request *mru.Request) (string, error) {
   600  	responseStream, err := c.updateResource(request)
   601  	if err != nil {
   602  		return "", err
   603  	}
   604  	defer responseStream.Close()
   605  
   606  	body, err := ioutil.ReadAll(responseStream)
   607  	if err != nil {
   608  		return "", err
   609  	}
   610  
   611  	var manifestAddress string
   612  	if err = json.Unmarshal(body, &manifestAddress); err != nil {
   613  		return "", err
   614  	}
   615  	return manifestAddress, nil
   616  }
   617  
   618  //UpdateResource允许您设置内容的新版本
   619  func (c *Client) UpdateResource(request *mru.Request) error {
   620  	_, err := c.updateResource(request)
   621  	return err
   622  }
   623  
   624  func (c *Client) updateResource(request *mru.Request) (io.ReadCloser, error) {
   625  	body, err := request.MarshalJSON()
   626  	if err != nil {
   627  		return nil, err
   628  	}
   629  
   630  	req, err := http.NewRequest("POST", c.Gateway+"/bzz-resource:/", bytes.NewBuffer(body))
   631  	if err != nil {
   632  		return nil, err
   633  	}
   634  
   635  	res, err := http.DefaultClient.Do(req)
   636  	if err != nil {
   637  		return nil, err
   638  	}
   639  
   640  	return res.Body, nil
   641  
   642  }
   643  
   644  //GetResource返回包含资源原始内容的字节流
   645  //ManifestAddressOrDomain是您在CreateResource或其解析程序的ENS域中获得的地址。
   646  //指向那个地址
   647  func (c *Client) GetResource(manifestAddressOrDomain string) (io.ReadCloser, error) {
   648  
   649  	res, err := http.Get(c.Gateway + "/bzz-resource:/" + manifestAddressOrDomain)
   650  	if err != nil {
   651  		return nil, err
   652  	}
   653  	return res.Body, nil
   654  
   655  }
   656  
   657  //GetResourceMetadata返回一个描述可变资源的结构
   658  //ManifestAddressOrDomain是您在CreateResource或其解析程序的ENS域中获得的地址。
   659  //指向那个地址
   660  func (c *Client) GetResourceMetadata(manifestAddressOrDomain string) (*mru.Request, error) {
   661  
   662  	responseStream, err := c.GetResource(manifestAddressOrDomain + "/meta")
   663  	if err != nil {
   664  		return nil, err
   665  	}
   666  	defer responseStream.Close()
   667  
   668  	body, err := ioutil.ReadAll(responseStream)
   669  	if err != nil {
   670  		return nil, err
   671  	}
   672  
   673  	var metadata mru.Request
   674  	if err := metadata.UnmarshalJSON(body); err != nil {
   675  		return nil, err
   676  	}
   677  	return &metadata, nil
   678  }
   679