2015-12-30 18:20:41 +01:00
|
|
|
package image
|
2015-07-28 14:35:24 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2015-07-21 13:30:32 -07:00
|
|
|
"errors"
|
2015-07-28 14:35:24 -04:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2016-01-26 13:30:58 -05:00
|
|
|
"net/url"
|
2015-07-28 14:35:24 -04:00
|
|
|
"strings"
|
|
|
|
|
2015-11-18 14:20:54 -08:00
|
|
|
"github.com/docker/distribution/digest"
|
2016-01-26 13:30:58 -05:00
|
|
|
"github.com/docker/distribution/registry/api/errcode"
|
2015-09-23 19:42:08 -04:00
|
|
|
"github.com/docker/docker/api/server/httputils"
|
2015-09-05 15:49:06 -04:00
|
|
|
"github.com/docker/docker/builder/dockerfile"
|
2015-10-12 14:38:12 -07:00
|
|
|
derr "github.com/docker/docker/errors"
|
2015-07-28 14:35:24 -04:00
|
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
|
|
"github.com/docker/docker/pkg/streamformatter"
|
2015-12-04 13:55:15 -08:00
|
|
|
"github.com/docker/docker/reference"
|
2015-07-28 14:35:24 -04:00
|
|
|
"github.com/docker/docker/runconfig"
|
2016-01-04 19:05:26 -05:00
|
|
|
"github.com/docker/engine-api/types"
|
|
|
|
"github.com/docker/engine-api/types/container"
|
2015-09-29 17:32:07 -04:00
|
|
|
"golang.org/x/net/context"
|
2015-07-28 14:35:24 -04:00
|
|
|
)
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) postCommit(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.CheckForJSON(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
cname := r.Form.Get("container")
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
pause := httputils.BoolValue(r, "pause")
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
2015-07-28 14:35:24 -04:00
|
|
|
if r.FormValue("pause") == "" && version.GreaterThanOrEqualTo("1.13") {
|
|
|
|
pause = true
|
|
|
|
}
|
|
|
|
|
2016-01-07 16:18:34 -08:00
|
|
|
c, _, _, err := runconfig.DecodeContainerConfig(r.Body)
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil && err != io.EOF { //Do not fail if body is empty.
|
|
|
|
return err
|
|
|
|
}
|
2015-12-09 20:07:53 +01:00
|
|
|
if c == nil {
|
2015-12-18 13:36:17 -05:00
|
|
|
c = &container.Config{}
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
|
2016-02-22 10:53:47 -08:00
|
|
|
if !s.backend.Exists(cname) {
|
2015-10-12 14:38:12 -07:00
|
|
|
return derr.ErrorCodeNoSuchContainer.WithArgs(cname)
|
2015-09-06 13:26:40 -04:00
|
|
|
}
|
|
|
|
|
2015-12-09 20:07:53 +01:00
|
|
|
newConfig, err := dockerfile.BuildFromConfig(c, r.Form["changes"])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
commitCfg := &types.ContainerCommitConfig{
|
|
|
|
Pause: pause,
|
|
|
|
Repo: r.Form.Get("repo"),
|
|
|
|
Tag: r.Form.Get("tag"),
|
|
|
|
Author: r.Form.Get("author"),
|
|
|
|
Comment: r.Form.Get("comment"),
|
|
|
|
Config: newConfig,
|
|
|
|
MergeConfigs: true,
|
|
|
|
}
|
|
|
|
|
2016-02-22 10:53:47 -08:00
|
|
|
imgID, err := s.backend.Commit(cname, commitCfg)
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
return httputils.WriteJSON(w, http.StatusCreated, &types.ContainerCommitResponse{
|
2015-09-06 13:26:40 -04:00
|
|
|
ID: string(imgID),
|
2015-07-28 14:35:24 -04:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates an image from Pull or from Import
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2015-08-20 04:01:50 +00:00
|
|
|
image = r.Form.Get("fromImage")
|
|
|
|
repo = r.Form.Get("repo")
|
|
|
|
tag = r.Form.Get("tag")
|
|
|
|
message = r.Form.Get("message")
|
2016-01-24 18:02:21 +01:00
|
|
|
err error
|
|
|
|
output = ioutils.NewWriteFlusher(w)
|
2015-07-28 14:35:24 -04:00
|
|
|
)
|
2015-11-02 16:11:28 -08:00
|
|
|
defer output.Close()
|
2015-07-28 14:35:24 -04:00
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
if image != "" { //pull
|
2015-11-18 14:20:54 -08:00
|
|
|
// Special case: "pull -a" may send an image name with a
|
|
|
|
// trailing :. This is ugly, but let's not break API
|
|
|
|
// compatibility.
|
|
|
|
image = strings.TrimSuffix(image, ":")
|
|
|
|
|
|
|
|
var ref reference.Named
|
|
|
|
ref, err = reference.ParseNamed(image)
|
|
|
|
if err == nil {
|
|
|
|
if tag != "" {
|
|
|
|
// The "tag" could actually be a digest.
|
|
|
|
var dgst digest.Digest
|
|
|
|
dgst, err = digest.ParseDigest(tag)
|
|
|
|
if err == nil {
|
|
|
|
ref, err = reference.WithDigest(ref, dgst)
|
|
|
|
} else {
|
|
|
|
ref, err = reference.WithTag(ref, tag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
metaHeaders := map[string][]string{}
|
|
|
|
for k, v := range r.Header {
|
|
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
|
|
metaHeaders[k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-24 18:02:21 +01:00
|
|
|
authEncoded := r.Header.Get("X-Registry-Auth")
|
|
|
|
authConfig := &types.AuthConfig{}
|
|
|
|
if authEncoded != "" {
|
|
|
|
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
|
|
|
if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
|
|
|
|
// for a pull it is not an error if no auth was given
|
|
|
|
// to increase compatibility with the existing api it is defaulting to be empty
|
|
|
|
authConfig = &types.AuthConfig{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-22 10:53:47 -08:00
|
|
|
err = s.backend.PullImage(ref, metaHeaders, authConfig, output)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
}
|
2016-01-26 13:30:58 -05:00
|
|
|
// Check the error from pulling an image to make sure the request
|
|
|
|
// was authorized. Modify the status if the request was
|
|
|
|
// unauthorized to respond with 401 rather than 500.
|
|
|
|
if err != nil && isAuthorizedError(err) {
|
|
|
|
err = errcode.ErrorCodeUnauthorized.WithMessage(fmt.Sprintf("Authentication is required: %s", err))
|
|
|
|
}
|
2015-11-18 14:20:54 -08:00
|
|
|
} else { //import
|
|
|
|
var newRef reference.Named
|
|
|
|
if repo != "" {
|
|
|
|
var err error
|
|
|
|
newRef, err = reference.ParseNamed(repo)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-07-28 14:35:24 -04:00
|
|
|
|
2015-12-10 11:01:34 -08:00
|
|
|
if _, isCanonical := newRef.(reference.Canonical); isCanonical {
|
2015-11-18 14:20:54 -08:00
|
|
|
return errors.New("cannot import digest reference")
|
|
|
|
}
|
2015-07-28 14:35:24 -04:00
|
|
|
|
2015-11-18 14:20:54 -08:00
|
|
|
if tag != "" {
|
|
|
|
newRef, err = reference.WithTag(newRef, tag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
src := r.Form.Get("fromSrc")
|
|
|
|
|
|
|
|
// 'err' MUST NOT be defined within this block, we need any error
|
|
|
|
// generated from the download to be available to the output
|
|
|
|
// stream processing below
|
2015-12-18 13:36:17 -05:00
|
|
|
var newConfig *container.Config
|
|
|
|
newConfig, err = dockerfile.BuildFromConfig(&container.Config{}, r.Form["changes"])
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-02-22 10:53:47 -08:00
|
|
|
err = s.backend.ImportImage(src, newRef, message, r.Body, output, newConfig)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
if !output.Flushed() {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sf := streamformatter.NewJSONStreamFormatter()
|
|
|
|
output.Write(sf.FormatError(err))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-07-28 14:35:24 -04:00
|
|
|
metaHeaders := map[string][]string{}
|
|
|
|
for k, v := range r.Header {
|
|
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
|
|
metaHeaders[k] = v
|
|
|
|
}
|
|
|
|
}
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
2015-12-11 20:11:42 -08:00
|
|
|
authConfig := &types.AuthConfig{}
|
2015-07-28 14:35:24 -04:00
|
|
|
|
|
|
|
authEncoded := r.Header.Get("X-Registry-Auth")
|
|
|
|
if authEncoded != "" {
|
|
|
|
// the new format is to handle the authConfig as a header
|
|
|
|
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
|
|
|
if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
|
|
|
|
// to increase compatibility to existing api it is defaulting to be empty
|
2015-12-11 20:11:42 -08:00
|
|
|
authConfig = &types.AuthConfig{}
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// the old format is supported for compatibility if there was no authConfig header
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
|
|
|
return fmt.Errorf("Bad parameters and missing X-Registry-Auth: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-18 14:20:54 -08:00
|
|
|
ref, err := reference.ParseNamed(vars["name"])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tag := r.Form.Get("tag")
|
|
|
|
if tag != "" {
|
|
|
|
// Push by digest is not supported, so only tags are supported.
|
|
|
|
ref, err = reference.WithTag(ref, tag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-28 14:35:24 -04:00
|
|
|
output := ioutils.NewWriteFlusher(w)
|
2015-11-02 16:11:28 -08:00
|
|
|
defer output.Close()
|
2015-07-28 14:35:24 -04:00
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
2016-02-22 10:53:47 -08:00
|
|
|
if err := s.backend.PushImage(ref, metaHeaders, authConfig, output); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
if !output.Flushed() {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sf := streamformatter.NewJSONStreamFormatter()
|
|
|
|
output.Write(sf.FormatError(err))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/x-tar")
|
|
|
|
|
|
|
|
output := ioutils.NewWriteFlusher(w)
|
2015-11-02 16:11:28 -08:00
|
|
|
defer output.Close()
|
2015-07-28 14:35:24 -04:00
|
|
|
var names []string
|
|
|
|
if name, ok := vars["name"]; ok {
|
|
|
|
names = []string{name}
|
|
|
|
} else {
|
|
|
|
names = r.Form["names"]
|
|
|
|
}
|
|
|
|
|
2016-02-22 10:53:47 -08:00
|
|
|
if err := s.backend.ExportImage(names, output); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
if !output.Flushed() {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sf := streamformatter.NewJSONStreamFormatter()
|
|
|
|
output.Write(sf.FormatError(err))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-01-27 17:09:42 -05:00
|
|
|
func (s *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2016-02-03 21:31:47 -05:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
quiet := httputils.BoolValueOrDefault(r, "quiet", true)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2016-02-22 10:53:47 -08:00
|
|
|
return s.backend.LoadImage(r.Body, w, quiet)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
name := vars["name"]
|
2015-08-15 00:30:25 -07:00
|
|
|
|
2015-11-03 17:19:18 -08:00
|
|
|
if strings.TrimSpace(name) == "" {
|
2015-08-15 00:30:25 -07:00
|
|
|
return fmt.Errorf("image name cannot be blank")
|
|
|
|
}
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
force := httputils.BoolValue(r, "force")
|
|
|
|
prune := !httputils.BoolValue(r, "noprune")
|
2015-07-28 14:35:24 -04:00
|
|
|
|
2016-02-22 10:53:47 -08:00
|
|
|
list, err := s.backend.ImageDelete(name, force, prune)
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
return httputils.WriteJSON(w, http.StatusOK, list)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2016-02-22 10:53:47 -08:00
|
|
|
imageInspect, err := s.backend.LookupImage(vars["name"])
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
return httputils.WriteJSON(w, http.StatusOK, imageInspect)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: The filter parameter could just be a match filter
|
2016-02-22 10:53:47 -08:00
|
|
|
images, err := s.backend.Images(r.Form.Get("filters"), r.Form.Get("filter"), httputils.BoolValue(r, "all"))
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
return httputils.WriteJSON(w, http.StatusOK, images)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-07-28 14:35:24 -04:00
|
|
|
name := vars["name"]
|
2016-02-22 10:53:47 -08:00
|
|
|
history, err := s.backend.ImageHistory(name)
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-09-23 19:42:08 -04:00
|
|
|
return httputils.WriteJSON(w, http.StatusOK, history)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
repo := r.Form.Get("repo")
|
|
|
|
tag := r.Form.Get("tag")
|
2015-11-18 14:20:54 -08:00
|
|
|
newTag, err := reference.WithName(repo)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if tag != "" {
|
|
|
|
if newTag, err = reference.WithTag(newTag, tag); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2016-02-22 10:53:47 -08:00
|
|
|
if err := s.backend.TagImage(newTag, vars["name"]); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-12-30 18:20:41 +01:00
|
|
|
func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
2015-09-23 19:42:08 -04:00
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
2015-07-28 14:35:24 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
var (
|
2015-12-11 20:11:42 -08:00
|
|
|
config *types.AuthConfig
|
2015-07-28 14:35:24 -04:00
|
|
|
authEncoded = r.Header.Get("X-Registry-Auth")
|
|
|
|
headers = map[string][]string{}
|
|
|
|
)
|
|
|
|
|
|
|
|
if authEncoded != "" {
|
|
|
|
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
|
|
|
if err := json.NewDecoder(authJSON).Decode(&config); err != nil {
|
|
|
|
// for a search it is not an error if no auth was given
|
|
|
|
// to increase compatibility with the existing api it is defaulting to be empty
|
2015-12-11 20:11:42 -08:00
|
|
|
config = &types.AuthConfig{}
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for k, v := range r.Header {
|
|
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
|
|
headers[k] = v
|
|
|
|
}
|
|
|
|
}
|
2016-02-22 10:53:47 -08:00
|
|
|
query, err := s.backend.SearchRegistryForImages(r.Form.Get("term"), config, headers)
|
2015-07-28 14:35:24 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-09-23 19:42:08 -04:00
|
|
|
return httputils.WriteJSON(w, http.StatusOK, query.Results)
|
2015-07-28 14:35:24 -04:00
|
|
|
}
|
2016-01-26 13:30:58 -05:00
|
|
|
|
|
|
|
func isAuthorizedError(err error) bool {
|
|
|
|
if urlError, ok := err.(*url.Error); ok {
|
|
|
|
err = urlError.Err
|
|
|
|
}
|
|
|
|
|
|
|
|
if dError, ok := err.(errcode.Error); ok {
|
|
|
|
if dError.ErrorCode() == errcode.ErrorCodeUnauthorized {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|