<commit_before>package docker

import (
	"fmt"
	"io"
	"io/ioutil"
	"mime"
	"net/http"
	"net/url"
	"os"
	"strconv"

	"github.com/Sirupsen/logrus"
	"github.com/containers/image/docker/reference"
	"github.com/containers/image/manifest"
	"github.com/containers/image/types"
	"github.com/docker/distribution/registry/client"
	"github.com/opencontainers/go-digest"
	"github.com/pkg/errors"
)

type dockerImageSource struct {
	ref                        dockerReference
	requestedManifestMIMETypes []string
	c                          *dockerClient
	// State
	cachedManifest         []byte // nil if not loaded yet
	cachedManifestMIMEType string // Only valid if cachedManifest != nil
}

// newImageSource creates a new ImageSource for the specified image reference,
// asking the backend to use a manifest from requestedManifestMIMETypes if possible.
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) {
	c, err := newDockerClient(ctx, ref, false, "pull")
	if err != nil {
		return nil, err
	}
	if requestedManifestMIMETypes == nil {
		requestedManifestMIMETypes = manifest.DefaultRequestedManifestMIMETypes
	}
	supportedMIMEs := supportedManifestMIMETypesMap()
	acceptableRequestedMIMEs := false
	for _, mtrequested := range requestedManifestMIMETypes {
		if supportedMIMEs[mtrequested] {
			acceptableRequestedMIMEs = true
			break
		}
	}
	if !acceptableRequestedMIMEs {
		requestedManifestMIMETypes = manifest.DefaultRequestedManifestMIMETypes
	}
	return &dockerImageSource{
		ref: ref,
		requestedManifestMIMETypes: requestedManifestMIMETypes,
		c: c,
	}, nil
}

// Reference returns the reference used to set up this source, _as specified by the user_
// (not as the image itself, or its underlying storage, claims).  This can be used e.g. to determine which public keys are trusted for this image.
func (s *dockerImageSource) Reference() types.ImageReference {
	return s.ref
}

// Close removes resources associated with an initialized ImageSource, if any.
func (s *dockerImageSource) Close() error {
	return nil
}

// simplifyContentType drops parameters from a HTTP media type (see https://tools.ietf.org/html/rfc7231#section-3.1.1.1)
// Alternatively, an empty string is returned unchanged, and invalid values are "simplified" to an empty string.
func simplifyContentType(contentType string) string {
	if contentType == "" {
		return contentType
	}
	mimeType, _, err := mime.ParseMediaType(contentType)
	if err != nil {
		return ""
	}
	return mimeType
}

// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
// It may use a remote (= slow) service.
func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
	err := s.ensureManifestIsLoaded()
	if err != nil {
		return nil, "", err
	}
	return s.cachedManifest, s.cachedManifestMIMEType, nil
}

func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) {
	path := fmt.Sprintf(manifestPath, reference.Path(s.ref.ref), tagOrDigest)
	headers := make(map[string][]string)
	headers["Accept"] = s.requestedManifestMIMETypes
	res, err := s.c.makeRequest("GET", path, headers, nil)
	if err != nil {
		return nil, "", err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return nil, "", client.HandleErrorResponse(res)
	}
	manblob, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, "", err
	}
	return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil
}

// GetTargetManifest returns an image's manifest given a digest.
// This is mainly used to retrieve a single image's manifest out of a manifest list.
func (s *dockerImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) {
	return s.fetchManifest(digest.String())
}

// ensureManifestIsLoaded sets s.cachedManifest and s.cachedManifestMIMEType
//
// ImageSource implementations are not required or expected to do any caching,
// but because our signatures are “attached” to the manifest digest,
// we need to ensure that the digest of the manifest returned by GetManifest
// and used by GetSignatures are consistent, otherwise we would get spurious
// signature verification failures when pulling while a tag is being updated.
func (s *dockerImageSource) ensureManifestIsLoaded() error {
	if s.cachedManifest != nil {
		return nil
	}

	reference, err := s.ref.tagOrDigest()
	if err != nil {
		return err
	}

	manblob, mt, err := s.fetchManifest(reference)
	if err != nil {
		return err
	}
	// We might validate manblob against the Docker-Content-Digest header here to protect against transport errors.
	s.cachedManifest = manblob
	s.cachedManifestMIMEType = mt
	return nil
}

func (s *dockerImageSource) getExternalBlob(urls []string) (io.ReadCloser, int64, error) {
	var (
		resp *http.Response
		err  error
	)
	for _, url := range urls {
		resp, err = s.c.makeRequestToResolvedURL("GET", url, nil, nil, -1, false)
		if err == nil {
			if resp.StatusCode != http.StatusOK {
				err = errors.Errorf("error fetching external blob from %q: %d", url, resp.StatusCode)
				logrus.Debug(err)
				continue
			}
		}
	}
	if resp.Body != nil && err == nil {
		return resp.Body, getBlobSize(resp), nil
	}
	return nil, 0, err
}

func getBlobSize(resp *http.Response) int64 {
	size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
	if err != nil {
		size = -1
	}
	return size
}

// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
func (s *dockerImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, error) {
	if len(info.URLs) != 0 {
		return s.getExternalBlob(info.URLs)
	}

	path := fmt.Sprintf(blobsPath, reference.Path(s.ref.ref), info.Digest.String())
	logrus.Debugf("Downloading %s", path)
	res, err := s.c.makeRequest("GET", path, nil, nil)
	if err != nil {
		return nil, 0, err
	}
	if res.StatusCode != http.StatusOK {
		// print url also
		return nil, 0, errors.Errorf("Invalid status code returned when fetching blob %d", res.StatusCode)
	}
	return res.Body, getBlobSize(res), nil
}

func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
	if err := s.c.detectProperties(); err != nil {
		return nil, err
	}
	switch {
	case s.c.signatureBase != nil:
		return s.getSignaturesFromLookaside()
	case s.c.supportsSignatures:
		return s.getSignaturesFromAPIExtension()
	default:
		return [][]byte{}, nil
	}
}

// getSignaturesFromLookaside implements GetSignatures() from the lookaside location configured in s.c.signatureBase,
// which is not nil.
func (s *dockerImageSource) getSignaturesFromLookaside() ([][]byte, error) {
	if err := s.ensureManifestIsLoaded(); err != nil {
		return nil, err
	}
	manifestDigest, err := manifest.Digest(s.cachedManifest)
	if err != nil {
		return nil, err
	}

	// NOTE: Keep this in sync with docs/signature-protocols.md!
	signatures := [][]byte{}
	for i := 0; ; i++ {
		url := signatureStorageURL(s.c.signatureBase, manifestDigest, i)
		if url == nil {
			return nil, errors.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
		}
		signature, missing, err := s.getOneSignature(url)
		if err != nil {
			return nil, err
		}
		if missing {
			break
		}
		signatures = append(signatures, signature)
	}
	return signatures, nil
}

// getOneSignature downloads one signature from url.
// If it successfully determines that the signature does not exist, returns with missing set to true and error set to nil.
// NOTE: Keep this in sync with docs/signature-protocols.md!
func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, missing bool, err error) {
	switch url.Scheme {
	case "file":
		logrus.Debugf("Reading %s", url.Path)
		sig, err := ioutil.ReadFile(url.Path)
		if err != nil {
			if os.IsNotExist(err) {
				return nil, true, nil
			}
			return nil, false, err
		}
		return sig, false, nil

	case "http", "https":
		logrus.Debugf("GET %s", url)
		res, err := s.c.client.Get(url.String())
		if err != nil {
			return nil, false, err
		}
		defer res.Body.Close()
		if res.StatusCode == http.StatusNotFound {
			return nil, true, nil
		} else if res.StatusCode != http.StatusOK {
			return nil, false, errors.Errorf("Error reading signature from %s: status %d", url.String(), res.StatusCode)
		}
		sig, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return nil, false, err
		}
		return sig, false, nil

	default:
		return nil, false, errors.Errorf("Unsupported scheme when reading signature from %s", url.String())
	}
}

// getSignaturesFromAPIExtension implements GetSignatures() using the X-Registry-Supports-Signatures API extension.
func (s *dockerImageSource) getSignaturesFromAPIExtension() ([][]byte, error) {
	if err := s.ensureManifestIsLoaded(); err != nil {
		return nil, err
	}
	manifestDigest, err := manifest.Digest(s.cachedManifest)
	if err != nil {
		return nil, err
	}

	parsedBody, err := s.c.getExtensionsSignatures(s.ref, manifestDigest)
	if err != nil {
		return nil, err
	}

	var sigs [][]byte
	for _, sig := range parsedBody.Signatures {
		if sig.Version == extensionSignatureSchemaVersion && sig.Type == extensionSignatureTypeAtomic {
			sigs = append(sigs, sig.Content)
		}
	}
	return sigs, nil
}

// deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
	c, err := newDockerClient(ctx, ref, true, "push")
	if err != nil {
		return err
	}

	// When retrieving the digest from a registry >= 2.3 use the following header:
	//   "Accept": "application/vnd.docker.distribution.manifest.v2+json"
	headers := make(map[string][]string)
	headers["Accept"] = []string{manifest.DockerV2Schema2MediaType}

	refTail, err := ref.tagOrDigest()
	if err != nil {
		return err
	}
	getPath := fmt.Sprintf(manifestPath, reference.Path(ref.ref), refTail)
	get, err := c.makeRequest("GET", getPath, headers, nil)
	if err != nil {
		return err
	}
	defer get.Body.Close()
	manifestBody, err := ioutil.ReadAll(get.Body)
	if err != nil {
		return err
	}
	switch get.StatusCode {
	case http.StatusOK:
	case http.StatusNotFound:
		return errors.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry", ref.ref)
	default:
		return errors.Errorf("Failed to delete %v: %s (%v)", ref.ref, manifestBody, get.Status)
	}

	digest := get.Header.Get("Docker-Content-Digest")
	deletePath := fmt.Sprintf(manifestPath, reference.Path(ref.ref), digest)

	// When retrieving the digest from a registry >= 2.3 use the following header:
	//   "Accept": "application/vnd.docker.distribution.manifest.v2+json"
	delete, err := c.makeRequest("DELETE", deletePath, headers, nil)
	if err != nil {
		return err
	}
	defer delete.Body.Close()

	body, err := ioutil.ReadAll(delete.Body)
	if err != nil {
		return err
	}
	if delete.StatusCode != http.StatusAccepted {
		return errors.Errorf("Failed to delete %v: %s (%v)", deletePath, string(body), delete.Status)
	}

	if c.signatureBase != nil {
		manifestDigest, err := manifest.Digest(manifestBody)
		if err != nil {
			return err
		}

		for i := 0; ; i++ {
			url := signatureStorageURL(c.signatureBase, manifestDigest, i)
			if url == nil {
				return errors.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
			}
			missing, err := c.deleteOneSignature(url)
			if err != nil {
				return err
			}
			if missing {
				break
			}
		}
	}

	return nil
}
<commit_msg>Don’t load the manifest in GetSignatures if the digest is known<commit_after>package docker

import (
	"fmt"
	"io"
	"io/ioutil"
	"mime"
	"net/http"
	"net/url"
	"os"
	"strconv"

	"github.com/Sirupsen/logrus"
	"github.com/containers/image/docker/reference"
	"github.com/containers/image/manifest"
	"github.com/containers/image/types"
	"github.com/docker/distribution/registry/client"
	"github.com/opencontainers/go-digest"
	"github.com/pkg/errors"
)

type dockerImageSource struct {
	ref                        dockerReference
	requestedManifestMIMETypes []string
	c                          *dockerClient
	// State
	cachedManifest         []byte // nil if not loaded yet
	cachedManifestMIMEType string // Only valid if cachedManifest != nil
}

// newImageSource creates a new ImageSource for the specified image reference,
// asking the backend to use a manifest from requestedManifestMIMETypes if possible.
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) {
	c, err := newDockerClient(ctx, ref, false, "pull")
	if err != nil {
		return nil, err
	}
	if requestedManifestMIMETypes == nil {
		requestedManifestMIMETypes = manifest.DefaultRequestedManifestMIMETypes
	}
	supportedMIMEs := supportedManifestMIMETypesMap()
	acceptableRequestedMIMEs := false
	for _, mtrequested := range requestedManifestMIMETypes {
		if supportedMIMEs[mtrequested] {
			acceptableRequestedMIMEs = true
			break
		}
	}
	if !acceptableRequestedMIMEs {
		requestedManifestMIMETypes = manifest.DefaultRequestedManifestMIMETypes
	}
	return &dockerImageSource{
		ref: ref,
		requestedManifestMIMETypes: requestedManifestMIMETypes,
		c: c,
	}, nil
}

// Reference returns the reference used to set up this source, _as specified by the user_
// (not as the image itself, or its underlying storage, claims).  This can be used e.g. to determine which public keys are trusted for this image.
func (s *dockerImageSource) Reference() types.ImageReference {
	return s.ref
}

// Close removes resources associated with an initialized ImageSource, if any.
func (s *dockerImageSource) Close() error {
	return nil
}

// simplifyContentType drops parameters from a HTTP media type (see https://tools.ietf.org/html/rfc7231#section-3.1.1.1)
// Alternatively, an empty string is returned unchanged, and invalid values are "simplified" to an empty string.
func simplifyContentType(contentType string) string {
	if contentType == "" {
		return contentType
	}
	mimeType, _, err := mime.ParseMediaType(contentType)
	if err != nil {
		return ""
	}
	return mimeType
}

// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
// It may use a remote (= slow) service.
func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
	err := s.ensureManifestIsLoaded()
	if err != nil {
		return nil, "", err
	}
	return s.cachedManifest, s.cachedManifestMIMEType, nil
}

func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) {
	path := fmt.Sprintf(manifestPath, reference.Path(s.ref.ref), tagOrDigest)
	headers := make(map[string][]string)
	headers["Accept"] = s.requestedManifestMIMETypes
	res, err := s.c.makeRequest("GET", path, headers, nil)
	if err != nil {
		return nil, "", err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return nil, "", client.HandleErrorResponse(res)
	}
	manblob, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, "", err
	}
	return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil
}

// GetTargetManifest returns an image's manifest given a digest.
// This is mainly used to retrieve a single image's manifest out of a manifest list.
func (s *dockerImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) {
	return s.fetchManifest(digest.String())
}

// ensureManifestIsLoaded sets s.cachedManifest and s.cachedManifestMIMEType
//
// ImageSource implementations are not required or expected to do any caching,
// but because our signatures are “attached” to the manifest digest,
// we need to ensure that the digest of the manifest returned by GetManifest
// and used by GetSignatures are consistent, otherwise we would get spurious
// signature verification failures when pulling while a tag is being updated.
func (s *dockerImageSource) ensureManifestIsLoaded() error {
	if s.cachedManifest != nil {
		return nil
	}

	reference, err := s.ref.tagOrDigest()
	if err != nil {
		return err
	}

	manblob, mt, err := s.fetchManifest(reference)
	if err != nil {
		return err
	}
	// We might validate manblob against the Docker-Content-Digest header here to protect against transport errors.
	s.cachedManifest = manblob
	s.cachedManifestMIMEType = mt
	return nil
}

func (s *dockerImageSource) getExternalBlob(urls []string) (io.ReadCloser, int64, error) {
	var (
		resp *http.Response
		err  error
	)
	for _, url := range urls {
		resp, err = s.c.makeRequestToResolvedURL("GET", url, nil, nil, -1, false)
		if err == nil {
			if resp.StatusCode != http.StatusOK {
				err = errors.Errorf("error fetching external blob from %q: %d", url, resp.StatusCode)
				logrus.Debug(err)
				continue
			}
		}
	}
	if resp.Body != nil && err == nil {
		return resp.Body, getBlobSize(resp), nil
	}
	return nil, 0, err
}

func getBlobSize(resp *http.Response) int64 {
	size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
	if err != nil {
		size = -1
	}
	return size
}

// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
func (s *dockerImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, error) {
	if len(info.URLs) != 0 {
		return s.getExternalBlob(info.URLs)
	}

	path := fmt.Sprintf(blobsPath, reference.Path(s.ref.ref), info.Digest.String())
	logrus.Debugf("Downloading %s", path)
	res, err := s.c.makeRequest("GET", path, nil, nil)
	if err != nil {
		return nil, 0, err
	}
	if res.StatusCode != http.StatusOK {
		// print url also
		return nil, 0, errors.Errorf("Invalid status code returned when fetching blob %d", res.StatusCode)
	}
	return res.Body, getBlobSize(res), nil
}

func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
	if err := s.c.detectProperties(); err != nil {
		return nil, err
	}
	switch {
	case s.c.signatureBase != nil:
		return s.getSignaturesFromLookaside()
	case s.c.supportsSignatures:
		return s.getSignaturesFromAPIExtension()
	default:
		return [][]byte{}, nil
	}
}

// manifestDigest returns a digest of the manifest, either from the supplied reference or from a fetched manifest.
func (s *dockerImageSource) manifestDigest() (digest.Digest, error) {
	if digested, ok := s.ref.ref.(reference.Digested); ok {
		d := digested.Digest()
		if d.Algorithm() == digest.Canonical {
			return d, nil
		}
	}
	if err := s.ensureManifestIsLoaded(); err != nil {
		return "", err
	}
	return manifest.Digest(s.cachedManifest)
}

// getSignaturesFromLookaside implements GetSignatures() from the lookaside location configured in s.c.signatureBase,
// which is not nil.
func (s *dockerImageSource) getSignaturesFromLookaside() ([][]byte, error) {
	manifestDigest, err := s.manifestDigest()
	if err != nil {
		return nil, err
	}

	// NOTE: Keep this in sync with docs/signature-protocols.md!
	signatures := [][]byte{}
	for i := 0; ; i++ {
		url := signatureStorageURL(s.c.signatureBase, manifestDigest, i)
		if url == nil {
			return nil, errors.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
		}
		signature, missing, err := s.getOneSignature(url)
		if err != nil {
			return nil, err
		}
		if missing {
			break
		}
		signatures = append(signatures, signature)
	}
	return signatures, nil
}

// getOneSignature downloads one signature from url.
// If it successfully determines that the signature does not exist, returns with missing set to true and error set to nil.
// NOTE: Keep this in sync with docs/signature-protocols.md!
func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, missing bool, err error) {
	switch url.Scheme {
	case "file":
		logrus.Debugf("Reading %s", url.Path)
		sig, err := ioutil.ReadFile(url.Path)
		if err != nil {
			if os.IsNotExist(err) {
				return nil, true, nil
			}
			return nil, false, err
		}
		return sig, false, nil

	case "http", "https":
		logrus.Debugf("GET %s", url)
		res, err := s.c.client.Get(url.String())
		if err != nil {
			return nil, false, err
		}
		defer res.Body.Close()
		if res.StatusCode == http.StatusNotFound {
			return nil, true, nil
		} else if res.StatusCode != http.StatusOK {
			return nil, false, errors.Errorf("Error reading signature from %s: status %d", url.String(), res.StatusCode)
		}
		sig, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return nil, false, err
		}
		return sig, false, nil

	default:
		return nil, false, errors.Errorf("Unsupported scheme when reading signature from %s", url.String())
	}
}

// getSignaturesFromAPIExtension implements GetSignatures() using the X-Registry-Supports-Signatures API extension.
func (s *dockerImageSource) getSignaturesFromAPIExtension() ([][]byte, error) {
	manifestDigest, err := s.manifestDigest()
	if err != nil {
		return nil, err
	}

	parsedBody, err := s.c.getExtensionsSignatures(s.ref, manifestDigest)
	if err != nil {
		return nil, err
	}

	var sigs [][]byte
	for _, sig := range parsedBody.Signatures {
		if sig.Version == extensionSignatureSchemaVersion && sig.Type == extensionSignatureTypeAtomic {
			sigs = append(sigs, sig.Content)
		}
	}
	return sigs, nil
}

// deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
	c, err := newDockerClient(ctx, ref, true, "push")
	if err != nil {
		return err
	}

	// When retrieving the digest from a registry >= 2.3 use the following header:
	//   "Accept": "application/vnd.docker.distribution.manifest.v2+json"
	headers := make(map[string][]string)
	headers["Accept"] = []string{manifest.DockerV2Schema2MediaType}

	refTail, err := ref.tagOrDigest()
	if err != nil {
		return err
	}
	getPath := fmt.Sprintf(manifestPath, reference.Path(ref.ref), refTail)
	get, err := c.makeRequest("GET", getPath, headers, nil)
	if err != nil {
		return err
	}
	defer get.Body.Close()
	manifestBody, err := ioutil.ReadAll(get.Body)
	if err != nil {
		return err
	}
	switch get.StatusCode {
	case http.StatusOK:
	case http.StatusNotFound:
		return errors.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry", ref.ref)
	default:
		return errors.Errorf("Failed to delete %v: %s (%v)", ref.ref, manifestBody, get.Status)
	}

	digest := get.Header.Get("Docker-Content-Digest")
	deletePath := fmt.Sprintf(manifestPath, reference.Path(ref.ref), digest)

	// When retrieving the digest from a registry >= 2.3 use the following header:
	//   "Accept": "application/vnd.docker.distribution.manifest.v2+json"
	delete, err := c.makeRequest("DELETE", deletePath, headers, nil)
	if err != nil {
		return err
	}
	defer delete.Body.Close()

	body, err := ioutil.ReadAll(delete.Body)
	if err != nil {
		return err
	}
	if delete.StatusCode != http.StatusAccepted {
		return errors.Errorf("Failed to delete %v: %s (%v)", deletePath, string(body), delete.Status)
	}

	if c.signatureBase != nil {
		manifestDigest, err := manifest.Digest(manifestBody)
		if err != nil {
			return err
		}

		for i := 0; ; i++ {
			url := signatureStorageURL(c.signatureBase, manifestDigest, i)
			if url == nil {
				return errors.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
			}
			missing, err := c.deleteOneSignature(url)
			if err != nil {
				return err
			}
			if missing {
				break
			}
		}
	}

	return nil
}
<|endoftext|>


<commit_before>package crawler

import (
	"errors"
	"fmt"
	"github.com/dokterbob/ipfs-search/indexer"
	"github.com/dokterbob/ipfs-search/queue"
	"gopkg.in/ipfs/go-ipfs-api.v1"
	"io"
	"log"
	"net/http"
)

type CrawlerArgs struct {
	Hash       string
	Name       string
	Size       uint64
	ParentHash string
	ParentName string
}

type Crawler struct {
	sh *shell.Shell
	id *indexer.Indexer
	fq *queue.TaskQueue
	hq *queue.TaskQueue
}

func NewCrawler(sh *shell.Shell, id *indexer.Indexer, fq *queue.TaskQueue, hq *queue.TaskQueue) *Crawler {
	c := new(Crawler)
	c.sh = sh
	c.id = id
	c.fq = fq
	c.hq = hq
	return c
}

func hashUrl(hash string) string {
	return fmt.Sprintf("/ipfs/%s", hash)
}

// Helper function for creating reference structure
/*
	'<hash>': {
		'references': {
			'<parent_hash>': {
				'name': '<name>'
			}
		}
	}

	if (document_exists) {
		if (references_exists) {
			add_parent_hash to references
		} else {
			add references to document
		}
	} else {
		create document with references as only information
	}
*/
func construct_references(name string, parent_hash string, parent_name string) map[string]interface{} {
	references := map[string]interface{}{}

	if name != "" {
		references = map[string]interface{}{
			parent_hash: map[string]interface{}{
				"name":        name,
				"parent_name": parent_name,
			},
		}
	}

	return references
}

// Given a particular hash (file or directory), start crawling
func (c Crawler) CrawlHash(hash string, name string, parent_hash string, parent_name string) error {
	indexed, err := c.id.IsIndexed(hash)
	if err != nil {
		return err
	}

	if indexed {
		log.Printf("Already indexed '%s', skipping", hash)
		return nil
	}

	log.Printf("Crawling hash '%s' (%s)", hash, name)

	url := hashUrl(hash)

	list, err := c.sh.FileList(url)
	if err != nil {
		return err
	}

	switch list.Type {
	case "File":
		// Add to file crawl queue
		// Note: we're expecting no references here, see comment below
		args := CrawlerArgs{
			Hash: hash,
			Name: name,
			Size: list.Size,
		}

		err = c.fq.AddTask(args)
		if err != nil {
			// failed to send the task
			return err
		}
	case "Directory":
		// Index name and size for directory and directory items
		properties := map[string]interface{}{
			"links":      list.Links,
			"size":       list.Size,
			"references": construct_references(name, parent_hash, parent_name),
		}

		c.id.IndexItem("Directory", hash, properties)

		for _, link := range list.Links {
			args := CrawlerArgs{
				Hash:       link.Hash,
				Name:       link.Name,
				Size:       link.Size,
				ParentHash: hash,
				ParentName: name,
			}

			switch link.Type {
			case "File":
				// Add file to crawl queue
				err = c.fq.AddTask(args)
				if err != nil {
					// failed to send the task
					return err
				}

			case "Directory":
				// Add directory to crawl queue
				c.hq.AddTask(args)
				if err != nil {
					// failed to send the task
					return err
				}
			default:
				log.Printf("Type '%s' skipped for '%s'", list.Type, hash)
			}
		}
	default:
		log.Printf("Type '%s' skipped for '%s'", list.Type, hash)
	}

	log.Printf("Finished hash %s", hash)

	return nil
}

func (c Crawler) getMimeType(hash string) (string, error) {
	url := hashUrl(hash)
	response, err := c.sh.Cat(url)
	if err != nil {
		return "", err
	}

	defer response.Close()

	var data []byte
	data = make([]byte, 512)
	numread, err := response.Read(data)
	if err == io.EOF {
		return "", err
	}

	if numread == 0 {
		return "", errors.New("0 characters read, mime type detection failed")
	}

	// Sniffing only uses at most the first 512 bytes
	return http.DetectContentType(data), nil
}

// Crawl a single object, known to be a file
func (c Crawler) CrawlFile(hash string, name string, parent_hash string, parent_name string, size uint64) error {
	log.Printf("Crawling file %s\n", hash)

	var (
		mimetype string
		err      error
	)

	if size > 0 {
		mimetype, err = c.getMimeType(hash)
		if err != nil {
			return err
		}
	}

	properties := map[string]interface{}{
		"mimetype":   mimetype,
		"size":       size,
		"references": construct_references(name, parent_hash, parent_name),
	}

	c.id.IndexItem("File", hash, properties)

	log.Printf("Finished file %s", hash)

	return nil
}
<commit_msg>Metadata indexing with Apache Tika.<commit_after>package crawler

import (
	"encoding/json"
	"fmt"
	"github.com/dokterbob/ipfs-search/indexer"
	"github.com/dokterbob/ipfs-search/queue"
	"gopkg.in/ipfs/go-ipfs-api.v1"
	"log"
	"os/exec"
)

type CrawlerArgs struct {
	Hash       string
	Name       string
	Size       uint64
	ParentHash string
	ParentName string
}

type Crawler struct {
	sh *shell.Shell
	id *indexer.Indexer
	fq *queue.TaskQueue
	hq *queue.TaskQueue
}

func NewCrawler(sh *shell.Shell, id *indexer.Indexer, fq *queue.TaskQueue, hq *queue.TaskQueue) *Crawler {
	c := new(Crawler)
	c.sh = sh
	c.id = id
	c.fq = fq
	c.hq = hq
	return c
}

func hashUrl(hash string) string {
	return fmt.Sprintf("/ipfs/%s", hash)
}

// Helper function for creating reference structure
/*
	'<hash>': {
		'references': {
			'<parent_hash>': {
				'name': '<name>'
			}
		}
	}

	if (document_exists) {
		if (references_exists) {
			add_parent_hash to references
		} else {
			add references to document
		}
	} else {
		create document with references as only information
	}
*/
func construct_references(name string, parent_hash string, parent_name string) map[string]interface{} {
	references := map[string]interface{}{}

	if name != "" {
		references = map[string]interface{}{
			parent_hash: map[string]interface{}{
				"name":        name,
				"parent_name": parent_name,
			},
		}
	}

	return references
}

// Given a particular hash (file or directory), start crawling
func (c Crawler) CrawlHash(hash string, name string, parent_hash string, parent_name string) error {
	indexed, err := c.id.IsIndexed(hash)
	if err != nil {
		return err
	}

	if indexed {
		log.Printf("Already indexed '%s', skipping", hash)
		return nil
	}

	log.Printf("Crawling hash '%s' (%s)", hash, name)

	url := hashUrl(hash)

	list, err := c.sh.FileList(url)
	if err != nil {
		return err
	}

	switch list.Type {
	case "File":
		// Add to file crawl queue
		// Note: we're expecting no references here, see comment below
		args := CrawlerArgs{
			Hash: hash,
			Name: name,
			Size: list.Size,
		}

		err = c.fq.AddTask(args)
		if err != nil {
			// failed to send the task
			return err
		}
	case "Directory":
		// Index name and size for directory and directory items
		properties := map[string]interface{}{
			"links":      list.Links,
			"size":       list.Size,
			"references": construct_references(name, parent_hash, parent_name),
		}

		c.id.IndexItem("Directory", hash, properties)

		for _, link := range list.Links {
			args := CrawlerArgs{
				Hash:       link.Hash,
				Name:       link.Name,
				Size:       link.Size,
				ParentHash: hash,
				ParentName: name,
			}

			switch link.Type {
			case "File":
				// Add file to crawl queue
				err = c.fq.AddTask(args)
				if err != nil {
					// failed to send the task
					return err
				}

			case "Directory":
				// Add directory to crawl queue
				c.hq.AddTask(args)
				if err != nil {
					// failed to send the task
					return err
				}
			default:
				log.Printf("Type '%s' skipped for '%s'", list.Type, hash)
			}
		}
	default:
		log.Printf("Type '%s' skipped for '%s'", list.Type, hash)
	}

	log.Printf("Finished hash %s", hash)

	return nil
}

func getMetadata(path string, metadata *map[string]interface{}) error {
	cmd := exec.Command("tika", "-j", path)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}
	if err := cmd.Start(); err != nil {
		return err
	}

	if err := json.NewDecoder(stdout).Decode(&metadata); err != nil {
		return err
	}

	return cmd.Wait()
}

// Crawl a single object, known to be a file
func (c Crawler) CrawlFile(hash string, name string, parent_hash string, parent_name string, size uint64) error {
	log.Printf("Crawling file %s\n", hash)

	metadata := make(map[string]interface{})

	if size > 0 {
		var path string
		if name != "" {
			path = fmt.Sprintf("/ipfs/%s/%s", parent_hash, name)
		} else {
			path = fmt.Sprintf("/ipfs/%s", hash)
		}

		if err := getMetadata(path, &metadata); err != nil {
			return err
		}
	}

	metadata["size"] = size
	metadata["references"] = construct_references(name, parent_hash, parent_name)

	c.id.IndexItem("File", hash, metadata)

	log.Printf("Finished file %s", hash)

	return nil
}
<|endoftext|>


<commit_before>// Copyright (C) 2017 Jennal(jennalcn@gmail.com). All rights reserved.
//
// Licensed under the MIT License (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
// http://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.

package aop

import (
	"fmt"
	"testing"
	"time"
)

func TestAop(t *testing.T) {
	NewAspect().
		Retry(3).
		Delay(10 * time.Second).
		Repeat(5).
		Do(func() {
		fmt.Println("Test")
	})
}
<commit_msg>aop test<commit_after>// Copyright (C) 2017 Jennal(jennalcn@gmail.com). All rights reserved.
//
// Licensed under the MIT License (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
// http://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.

package aop

import (
	"fmt"
	"testing"
	"time"
)

func TestAop(t *testing.T) {
	NewAspect().
		Retry(3).
		Delay(10 * time.Second).
		Repeat(5).
		Do(func() {
			fmt.Println("Test")
		})
}

func TestSequence(t *testing.T) {
	Sequence(func(next chan bool, exit chan bool) int {
		t.Log(1, "=>", 1)
		next <- true
		return 1
	}, func(next chan bool, exit chan bool, a int) int {
		t.Log(2, "=>", a)
		next <- true
		return a + 1
	}, func(next chan bool, exit chan bool, a int) int {
		t.Log(3, "=>", a)
		next <- true
		return a + 1
	}, func(next chan bool, exit chan bool, a int) int {
		t.Log(4, "=>", a)
		exit <- true
		return a + 1
	}, func(next chan bool, exit chan bool, a int) {
		t.Log(5, "=>", a)
		next <- true
	})
}

func TestParallel(t *testing.T) {
	Parallel(func(complete chan bool) {
		t.Log(1)
		complete <- true
	}, func(complete chan bool) {
		t.Log(2)
		complete <- true
	}, func(complete chan bool) {
		t.Log(3)
		complete <- true
	}, func(complete chan bool) {
		t.Log(4)
		complete <- true
	}, func(complete chan bool) {
		t.Log(5)
		complete <- true
	})
}
<|endoftext|>


<commit_before>// +build darwin,!gendocs

/*
Copyright 2016 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package constants

var SupportedVMDrivers = [...]string{
	"virtualbox",
	"xhyve",
	"vmwarefusion",
}

var DefaultMountDir = "/Users"
<commit_msg>Add hyperkit to supported driver list<commit_after>// +build darwin,!gendocs

/*
Copyright 2016 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package constants

var SupportedVMDrivers = [...]string{
	"virtualbox",
	"xhyve",
	"vmwarefusion",
	"hyperkit",
}

var DefaultMountDir = "/Users"
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"

	"code.google.com/p/go.net/context"
	"github.com/gsempe/apns/core"
	"github.com/streadway/amqp"
)

// MsgPushNotification is the message send from the cli to the apns standalone client
// It is also defined in the counterpart file (apns/apns/main.go apns/cli/main.go)
type MsgPushNotification struct {
	Text  string `json:"text"`
	Token string `json:"token"`
}

var (
	sandbox  = flag.Bool("sandbox", false, "Use this flag to communicate with the sandbox and not the production")
	certFile = flag.String("cert", "apns-cert.pem", "The certificate file")
	keyFile  = flag.String("key", "apns-key.pem", "The key file")
)

func init() {
	flag.Parse()
}

func main() {

	var (
		gw  *apns.Gateway
		err error
	)
	conn, ch, msgs, err := initRabbitMQ()
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	defer ch.Close()

	ctx, _ := context.WithCancel(context.Background())
	if *sandbox {
		gw, err = apns.NewSandboxGateway(ctx, *certFile, *keyFile)
	} else {
		gw, err = apns.NewGateway(ctx, *certFile, *keyFile)
	}
	if err != nil {
		log.Fatalf("Failed to connect to gateway: %s", err)
		panic(err)
	}

	gw.Errors(func(pnr *apns.PushNotificationResponse) {
		log.Printf("Unable to send push notification with ID %d, error %s", pnr.Identifier, pnr.Error)
	})

	runningIdentifier := uint32(0)

	for d := range msgs {
		msg := MsgPushNotification{}
		err := json.Unmarshal(d.Body, &msg)
		if err != nil {
			continue
		}
		runningIdentifier++

		payload := apns.NewPayload()
		payload.Alert = msg.Text
		payload.Badge = int(runningIdentifier)
		pn := apns.NewPushNotification()
		pn.DeviceToken = msg.Token
		pn.Identifier = runningIdentifier
		pn.AddPayload(payload)

		gw.Send(pn)
	}
}

// initRabbitMQ initialize the queue to communicate with the cli
func initRabbitMQ() (*amqp.Connection, *amqp.Channel, <-chan amqp.Delivery, error) {

	failOnError := func(err error, msg string) {
		if err != nil {
			log.Fatalf("%s: %s", msg, err)
			panic(fmt.Sprintf("%s: %s", msg, err))
		}
	}

	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")

	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")

	q, err := ch.QueueDeclare(
		"pushnotif", // name
		true,        // durable
		false,       // delete when usused
		false,       // exclusive
		false,       // no-wait
		nil,         // arguments
	)
	failOnError(err, "Failed to declare a queue")

	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		true,   // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	failOnError(err, "Failed to register a consumer")

	if msgs == nil {
		log.Println("chan msgs is nil")
	}
	return conn, ch, msgs, err
}
<commit_msg>Delete goto fail - goto fail useless code<commit_after>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"

	"code.google.com/p/go.net/context"
	"github.com/gsempe/apns/core"
	"github.com/streadway/amqp"
)

// MsgPushNotification is the message send from the cli to the apns standalone client
// It is also defined in the counterpart file (apns/apns/main.go apns/cli/main.go)
type MsgPushNotification struct {
	Text  string `json:"text"`
	Token string `json:"token"`
}

var (
	sandbox  = flag.Bool("sandbox", false, "Use this flag to communicate with the sandbox and not the production")
	certFile = flag.String("cert", "apns-cert.pem", "The certificate file")
	keyFile  = flag.String("key", "apns-key.pem", "The key file")
)

func init() {
	flag.Parse()
}

func main() {

	var (
		gw  *apns.Gateway
		err error
	)
	conn, ch, msgs, err := initRabbitMQ()
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	defer ch.Close()

	ctx, _ := context.WithCancel(context.Background())
	if *sandbox {
		gw, err = apns.NewSandboxGateway(ctx, *certFile, *keyFile)
	} else {
		gw, err = apns.NewGateway(ctx, *certFile, *keyFile)
	}
	if err != nil {
		log.Fatalf("Failed to connect to gateway: %s", err)
	}

	gw.Errors(func(pnr *apns.PushNotificationResponse) {
		log.Printf("Unable to send push notification with ID %d, error %s", pnr.Identifier, pnr.Error)
	})

	runningIdentifier := uint32(0)

	for d := range msgs {
		msg := MsgPushNotification{}
		err := json.Unmarshal(d.Body, &msg)
		if err != nil {
			continue
		}
		runningIdentifier++

		payload := apns.NewPayload()
		payload.Alert = msg.Text
		payload.Badge = int(runningIdentifier)
		pn := apns.NewPushNotification()
		pn.DeviceToken = msg.Token
		pn.Identifier = runningIdentifier
		pn.AddPayload(payload)

		gw.Send(pn)
	}
}

// initRabbitMQ initialize the queue to communicate with the cli
func initRabbitMQ() (*amqp.Connection, *amqp.Channel, <-chan amqp.Delivery, error) {

	failOnError := func(err error, msg string) {
		if err != nil {
			log.Fatalf("%s: %s", msg, err)
			panic(fmt.Sprintf("%s: %s", msg, err))
		}
	}

	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")

	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")

	q, err := ch.QueueDeclare(
		"pushnotif", // name
		true,        // durable
		false,       // delete when usused
		false,       // exclusive
		false,       // no-wait
		nil,         // arguments
	)
	failOnError(err, "Failed to declare a queue")

	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		true,   // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	failOnError(err, "Failed to register a consumer")

	if msgs == nil {
		log.Println("chan msgs is nil")
	}
	return conn, ch, msgs, err
}
<|endoftext|>


<commit_before>// Package upload provides a re-usable file upload and storage utility for Ponzu
// systems to handle multipart form data.
package upload

import (
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/ponzu-cms/ponzu/system/db"
	"github.com/ponzu-cms/ponzu/system/item"
)

// StoreFiles stores file uploads at paths like /YYYY/MM/filename.ext
func StoreFiles(req *http.Request) (map[string]string, error) {
	err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
	if err != nil {
		return nil, err
	}

	ts := req.FormValue("timestamp") // timestamp in milliseconds since unix epoch

	if ts == "" {
		ts = fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) // Unix() returns seconds since unix epoch
	}

	// To use for FormValue name:urlPath
	urlPaths := make(map[string]string)

	if len(req.MultipartForm.File) == 0 {
		return urlPaths, nil
	}

	req.Form.Set("timestamp", ts)

	// get or create upload directory to save files from request
	pwd, err := os.Getwd()
	if err != nil {
		err := fmt.Errorf("Failed to locate current directory: %s", err)
		return nil, err
	}

	i, err := strconv.ParseInt(ts, 10, 64)
	if err != nil {
		return nil, err
	}

	tm := time.Unix(int64(i/1000), int64(i%1000))

	urlPathPrefix := "api"
	uploadDirName := "uploads"

	uploadDir := filepath.Join(pwd, uploadDirName, fmt.Sprintf("%d", tm.Year()), fmt.Sprintf("%02d", tm.Month()))
	err = os.MkdirAll(uploadDir, os.ModeDir|os.ModePerm)
	if err != nil {
		return nil, err
	}

	// loop over all files and save them to disk
	for name, fds := range req.MultipartForm.File {
		filename, err := item.NormalizeString(fds[0].Filename)
		if err != nil {
			return nil, err
		}

		src, err := fds[0].Open()
		if err != nil {
			err := fmt.Errorf("Couldn't open uploaded file: %s", err)
			return nil, err

		}
		defer src.Close()

		// check if file at path exists, if so, add timestamp to file
		absPath := filepath.Join(uploadDir, filename)

		if _, err := os.Stat(absPath); !os.IsNotExist(err) {
			filename = fmt.Sprintf("%d-%s", time.Now().Unix(), filename)
			absPath = filepath.Join(uploadDir, filename)
		}

		// save to disk (TODO: or check if S3 credentials exist, & save to cloud)
		dst, err := os.Create(absPath)
		if err != nil {
			err := fmt.Errorf("Failed to create destination file for upload: %s", err)
			return nil, err
		}

		// copy file from src to dst on disk
		var size int64
		if size, err = io.Copy(dst, src); err != nil {
			err := fmt.Errorf("Failed to copy uploaded file to destination: %s", err)
			return nil, err
		}

		// add name:urlPath to req.PostForm to be inserted into db
		urlPath := fmt.Sprintf("/%s/%s/%d/%02d/%s", urlPathPrefix, uploadDirName, tm.Year(), tm.Month(), filename)
		urlPaths[name] = urlPath

		// add upload information to db
		go storeFileInfo(size, filename, urlPath, fds)
	}

	return urlPaths, nil
}

func storeFileInfo(size int64, filename, urlPath string, fds []*multipart.FileHeader) {
	data := url.Values{
		"name":           []string{filename},
		"path":           []string{urlPath},
		"content_type":   []string{fds[0].Header.Get("Content-Type")},
		"content_length": []string{fmt.Sprintf("%d", size)},
	}

	_, err := db.SetUpload("__uploads:-1", data)
	if err != nil {
		log.Println("Error saving file upload record to database:", err)
	}
}
<commit_msg>Also handle uploads<commit_after>// Package upload provides a re-usable file upload and storage utility for Ponzu
// systems to handle multipart form data.
package upload

import (
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/ponzu-cms/ponzu/system/db"
	"github.com/ponzu-cms/ponzu/system/item"
)

// StoreFiles stores file uploads at paths like /YYYY/MM/filename.ext
func StoreFiles(req *http.Request) (map[string]string, error) {
	err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
	if err != nil {
		return nil, err
	}

	ts := req.FormValue("timestamp") // timestamp in milliseconds since unix epoch

	if ts == "" {
		ts = fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) // Unix() returns seconds since unix epoch
	}

	// To use for FormValue name:urlPath
	urlPaths := make(map[string]string)

	if len(req.MultipartForm.File) == 0 {
		return urlPaths, nil
	}

	req.Form.Set("timestamp", ts)

	// get or create upload directory to save files from request

	i, err := strconv.ParseInt(ts, 10, 64)
	if err != nil {
		return nil, err
	}

	tm := time.Unix(int64(i/1000), int64(i%1000))

	urlPathPrefix := "api"
	uploadDirName := "uploads"

	uploadDir := filepath.Join(cfg.UploadDir(), fmt.Sprintf("%d", tm.Year()), fmt.Sprintf("%02d", tm.Month()))
	err = os.MkdirAll(uploadDir, os.ModeDir|os.ModePerm)
	if err != nil {
		return nil, err
	}

	// loop over all files and save them to disk
	for name, fds := range req.MultipartForm.File {
		filename, err := item.NormalizeString(fds[0].Filename)
		if err != nil {
			return nil, err
		}

		src, err := fds[0].Open()
		if err != nil {
			err := fmt.Errorf("Couldn't open uploaded file: %s", err)
			return nil, err

		}
		defer src.Close()

		// check if file at path exists, if so, add timestamp to file
		absPath := filepath.Join(uploadDir, filename)

		if _, err := os.Stat(absPath); !os.IsNotExist(err) {
			filename = fmt.Sprintf("%d-%s", time.Now().Unix(), filename)
			absPath = filepath.Join(uploadDir, filename)
		}

		// save to disk (TODO: or check if S3 credentials exist, & save to cloud)
		dst, err := os.Create(absPath)
		if err != nil {
			err := fmt.Errorf("Failed to create destination file for upload: %s", err)
			return nil, err
		}

		// copy file from src to dst on disk
		var size int64
		if size, err = io.Copy(dst, src); err != nil {
			err := fmt.Errorf("Failed to copy uploaded file to destination: %s", err)
			return nil, err
		}

		// add name:urlPath to req.PostForm to be inserted into db
		urlPath := fmt.Sprintf("/%s/%s/%d/%02d/%s", urlPathPrefix, uploadDirName, tm.Year(), tm.Month(), filename)
		urlPaths[name] = urlPath

		// add upload information to db
		go storeFileInfo(size, filename, urlPath, fds)
	}

	return urlPaths, nil
}

func storeFileInfo(size int64, filename, urlPath string, fds []*multipart.FileHeader) {
	data := url.Values{
		"name":           []string{filename},
		"path":           []string{urlPath},
		"content_type":   []string{fds[0].Header.Get("Content-Type")},
		"content_length": []string{fmt.Sprintf("%d", size)},
	}

	_, err := db.SetUpload("__uploads:-1", data)
	if err != nil {
		log.Println("Error saving file upload record to database:", err)
	}
}
<|endoftext|>


<commit_before>// Copyright 2016-2021 The Libsacloud Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package libsacloud

// Version バージョン
const Version = "2.15.0"
<commit_msg>Bump to v2.15.1<commit_after>// Copyright 2016-2021 The Libsacloud Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package libsacloud

// Version バージョン
const Version = "2.15.1"
<|endoftext|>


<commit_before>package packet

import (
	"bytes"
	"encoding/binary"

	"github.com/jsimonetti/go-artnet/packet/code"
	"github.com/jsimonetti/go-artnet/version"
)

var _ ArtNetPacket = &ArtCommandPacket{}

// ArtCommandPacket contains an ArtCommand Packet.
//
// The ArtCommand packet is used to send property set style commands. The packet can be
// unicast or broadcast, the decision being application specific.
//
// The Data field contains the command text. The text is ASCII encoded and is null terminated
// and is case insensitive. It is legal, although inefficient, to set the Data array size to
// the maximum of 512 and null pad unused entries.
// The command text may contain multiple commands and adheres to the following syntax:
//
//   Command=Data&
//
// The ampersand is a break between commands. Also note that the text is capitalised for
// readability; it is case insensitive. Thus far, two commands are defined by Art-Net. It is
// anticipated that additional commands will be added as other manufacturers register commands
// which have industry wide relevance. These commands shall be transmitted with EstaMan = 0xFFFF.
//
// Packet Strategy:
//  Controller -  Receive:            Application Specific
//                Unicast Transmit:   Application Specific
//                Broadcast Transmit: Application Specific
//  Node -        Receive:            Application Specific
//                Unicast Transmit:   Application Specific
//                Broadcast Transmit: Application Specific
//  MediaServer - Receive:            Application Specific
//                Unicast Transmit:   Application Specific
//                Broadcast Transmit: Application Specific
type ArtCommandPacket struct {
	// Inherit the Header header
	Header

	// this packet type contains a version
	version [2]byte

	// estamanufacturer contains a code used to represent equipment manufacturer.
	estamanufacturer [2]byte

	// Length indicates the length of the data
	Length uint16

	// Data is an ASCII string, null terminated. Max length is 512 bytes including the null terminator
	Data string
}

// NewArtCommandPacket returns an ArtNetPacket with the correct OpCode
func NewArtCommandPacket() *ArtCommandPacket {
	return &ArtCommandPacket{
		Header: Header{
			OpCode: code.OpCommand,
			id:     ArtNet,
		},
		version:          version.Bytes(),
		estamanufacturer: [2]byte{0xff, 0xff},
	}
}

// MarshalBinary marshals an ArtCommandPacket into a byte slice.
func (p *ArtCommandPacket) MarshalBinary() ([]byte, error) {
	var buf bytes.Buffer
	if err := binary.Write(&buf, binary.BigEndian, p); err != nil {
		return nil, err
	}
	return buf.Bytes(), p.validate()
}

// UnmarshalBinary unmarshals the contents of a byte slice into an ArtCommandPacket.
//TODO
func (p *ArtCommandPacket) UnmarshalBinary(b []byte) error {
	return p.validate()
}

// artPacket is an empty method to sattisfy the ArtNetPacket interface.
func (p *ArtCommandPacket) validate() error {
	if p.OpCode != code.OpCommand {
		return errInvalidOpCode
	}
	return nil
}
<commit_msg>Additional docs for ArtCommand<commit_after>package packet

import (
	"bytes"
	"encoding/binary"

	"github.com/jsimonetti/go-artnet/packet/code"
	"github.com/jsimonetti/go-artnet/version"
)

var _ ArtNetPacket = &ArtCommandPacket{}

// ArtCommandPacket contains an ArtCommand Packet.
//
// The ArtCommand packet is used to send property set style commands. The packet can be
// unicast or broadcast, the decision being application specific.
//
// The Data field contains the command text. The text is ASCII encoded and is null terminated
// and is case insensitive. It is legal, although inefficient, to set the Data array size to
// the maximum of 512 and null pad unused entries.
// The command text may contain multiple commands and adheres to the following syntax:
//
//   Command=Data&
//
// The ampersand is a break between commands. Also note that the text is capitalised for
// readability; it is case insensitive. Thus far, two commands are defined by Art-Net. It is
// anticipated that additional commands will be added as other manufacturers register commands
// which have industry wide relevance. These commands shall be transmitted with EstaMan = 0xFFFF.
//
// SwoutText - This command is used to re-programme the label associated with the
//             ArtPollReply->Swout fields. Syntax: "SwoutText=Playback&"
// SwinText  - This command is used to re-programme the label associated with the
//             ArtPollReply->Swin fields. Syntax: "SwinText=Record&"
//
// Packet Strategy:
//  Controller -  Receive:            Application Specific
//                Unicast Transmit:   Application Specific
//                Broadcast Transmit: Application Specific
//  Node -        Receive:            Application Specific
//                Unicast Transmit:   Application Specific
//                Broadcast Transmit: Application Specific
//  MediaServer - Receive:            Application Specific
//                Unicast Transmit:   Application Specific
//                Broadcast Transmit: Application Specific
type ArtCommandPacket struct {
	// Inherit the Header header
	Header

	// this packet type contains a version
	version [2]byte

	// estamanufacturer contains a code used to represent equipment manufacturer.
	estamanufacturer [2]byte

	// Length indicates the length of the data
	Length uint16

	// Data is an ASCII string, null terminated. Max length is 512 bytes including the null terminator
	Data string
}

// NewArtCommandPacket returns an ArtNetPacket with the correct OpCode
func NewArtCommandPacket() *ArtCommandPacket {
	return &ArtCommandPacket{
		Header: Header{
			OpCode: code.OpCommand,
			id:     ArtNet,
		},
		version:          version.Bytes(),
		estamanufacturer: [2]byte{0xff, 0xff},
	}
}

// MarshalBinary marshals an ArtCommandPacket into a byte slice.
func (p *ArtCommandPacket) MarshalBinary() ([]byte, error) {
	var buf bytes.Buffer
	if err := binary.Write(&buf, binary.BigEndian, p); err != nil {
		return nil, err
	}
	return buf.Bytes(), p.validate()
}

// UnmarshalBinary unmarshals the contents of a byte slice into an ArtCommandPacket.
//TODO
func (p *ArtCommandPacket) UnmarshalBinary(b []byte) error {
	return p.validate()
}

// artPacket is an empty method to sattisfy the ArtNetPacket interface.
func (p *ArtCommandPacket) validate() error {
	if p.OpCode != code.OpCommand {
		return errInvalidOpCode
	}
	return nil
}
<|endoftext|>


<commit_before>package metainspector

import (
	"fmt"
	"net/url"
	"testing"
)

var msgFail = "%v function fails. Expects %v, returns %v"

func TestRoot(t *testing.T) {
	u, err := url.Parse("http://www.cloudcontrol.com/pricing")
	if err != nil {
		t.Errorf("%v", err)
	}
	if rootUrl := ExportRoot(u); rootUrl != "http://www.cloudcontrol.com" {
		t.Errorf(msgFail, "rootURL", "http://www.cloudcontrol.com", rootUrl)
	}
}

func TestFixURL(t *testing.T) {
	u, err := url.Parse("http://foo.bar")
	if err != nil {
		t.Errorf("%v", err)
	}
	u.Scheme, u.Host, u.Path = "", "", "www.cloudcontrol.com/pricing"
	fix := ExportFixURL(u)
	if fix.Scheme != "http" {
		t.Errorf(msgFail, "Scheme", "http", fix.Scheme)
	} else if fix.Host != "www.cloudcontrol.com" {
		t.Errorf(msgFail, "Host", "www.cloudcontrol.com", fix.Host)
	} else if fix.Path != "/pricing" {
		t.Errorf(msgFail, "Path", "/pricing", fix.Path)
	}
}

var mi = MetaInspector{url: "http://www.cloudcontrol.com",
	scheme:        "http",
	host:          "www.cloudontrol.com",
	rootUrl:       "http://www.cloudcontrol.com",
	title:         "CloudControl",
	language:      "en",
	author:        "CloudControl",
	description:   "PaaS company",
	generator:     "some generator",
	feed:          "http://www.cloudcontrol.com/feed",
	charset:       "utf-8",
	links:         []string{"http://foo.bar", "https://bar.foo"},
	images:        []string{"http://foo.jpg", "https://bar.png"},
	keywords:      []string{"cloud", "PaaS"},
	compatibility: map[string]string{"IE": "edge"},
}

func TestUrl(t *testing.T) {
	if url := mi.Url(); url != "http://www.cloudcontrol.com" {
		t.Errorf(msgFail, "Url", "www.google.com", url)
	}
}

func TestScheme(t *testing.T) {
	if scheme := mi.Scheme(); scheme != "http" {
		t.Errorf(msgFail, "Scheme", "http", scheme)
	}
}

func TestHost(t *testing.T) {
	if host := mi.Host(); host != "www.cloudontrol.com" {
		t.Errorf(msgFail, "Host", "www.cloudontrol.com", host)
	}
}

func TestRootURL(t *testing.T) {
	if rootUrl := mi.RootURL(); rootUrl != "http://www.cloudcontrol.com" {
		t.Errorf(msgFail, "RootURL", "http://www.cloudontrol.com", rootUrl)
	}
}

func TestTitle(t *testing.T) {
	if title := mi.Title(); title != "CloudControl" {
		t.Errorf(msgFail, "Title", "CloudControl", title)
	}
}

func TestLanguage(t *testing.T) {
	if language := mi.Language(); language != "en" {
		t.Errorf(msgFail, "Language", "en", language)
	}
}

func TestAuthor(t *testing.T) {
	if author := mi.Author(); author != "CloudControl" {
		t.Errorf(msgFail, "Author", "CloudControl", author)
	}
}

func TestDescription(t *testing.T) {
	if description := mi.Description(); description != "PaaS company" {
		t.Errorf(msgFail, "Description", "PaaS company", description)
	}
}

func TestGenerator(t *testing.T) {
	if generator := mi.Generator(); generator != "some generator" {
		t.Errorf(msgFail, "Generator", "some generator", generator)
	}
}

func TestFeed(t *testing.T) {
	if feed := mi.Feed(); feed != "http://www.cloudcontrol.com/feed" {
		t.Errorf(msgFail, "Feed", "http://www.cloudcontrol.com/feed", feed)
	}
}

func TestCharset(t *testing.T) {
	if charset := mi.Charset(); charset != "utf-8" {
		t.Errorf(msgFail, "Charset", "utf-8", charset)
	}
}

func TestLinks(t *testing.T) {
	l1 := fmt.Sprintf("%v", mi.Links())
	l2 := "[http://foo.bar https://bar.foo]"
	if l1 != l2 {
		t.Errorf(msgFail, "Links", l2, l1)
	}
}

func TestImages(t *testing.T) {
	i1 := fmt.Sprintf("%v", mi.Images())
	i2 := "[http://foo.jpg https://bar.png]"
	if i1 != i2 {
		t.Errorf(msgFail, "Images", i2, i1)
	}
}

func TestKeywords(t *testing.T) {
	k1 := fmt.Sprintf("%v", mi.Keywords())
	k2 := "[cloud PaaS]"
	if k1 != k2 {
		t.Errorf(msgFail, "Keywords", k2, k1)
	}
}

func TestCompatibility(t *testing.T) {
	c1 := fmt.Sprintf("%v", mi.Compatibility())
	c2 := "map[IE:edge]"
	if c1 != c2 {
		t.Errorf(msgFail, "Compatibility", c2, c1)
	}
}

func ExampleMetaInspector() {
	url := "http://www.cloudcontrol.com/pricing"
	MI, err := New(url)
	if err != nil {
		fmt.Printf("Error: %v", err)
	} else {
		fmt.Printf("\nURL: %s\n", MI.Url())
		fmt.Printf("Scheme: %s\n", MI.Scheme())
		fmt.Printf("Host: %s\n", MI.Host())
		fmt.Printf("Root: %s\n", MI.RootURL())
		fmt.Printf("Title: %s\n", MI.Title())
		fmt.Printf("Language: %s\n", MI.Language())
		fmt.Printf("Author: %s\n", MI.Author())
		fmt.Printf("Description: %s\n", MI.Description())
		fmt.Printf("Charset: %s\n", MI.Charset())
		fmt.Printf("Feed URL: %s\n", MI.Feed())
		fmt.Printf("Links: %v\n", MI.Links())
		fmt.Printf("Images: %v\n", MI.Images())
		fmt.Printf("Keywords: %v\n", MI.Keywords())
		fmt.Printf("Compatibility: %v\n", MI.Compatibility())
		// Output:
		//URL: http://www.cloudcontrol.com/pricing
		//Scheme: http
		//Host: www.cloudcontrol.com
		//Root: http://www.cloudcontrol.com
		//Title: cloudControl » Cloud App Platform » Pricing
		//Language: en
		//Author: cloudControl GmbH
		//Description: Cloud hosting secure, easy and fair: Highly available and scalable cloud hosting with no administraton hassle and pay as you go billing
		//Charset: utf-8
		//Feed URL: https://www.cloudcontrol.com/blog.rss
		//Links: [http://www.cloudcontrol.com/console/account/{{user.username}} http://www.cloudcontrol.com/ http://www.cloudcontrol.com/pricing http://www.cloudcontrol.com/dev-center http://www.cloudcontrol.com/add-ons http://www.cloudcontrol.com/blog http://www.cloudcontrol.com/console http://www.cloudcontrol.com/pricing/calculator http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com/sign-up http://www.cloudcontrol.com/pricing/calculator http://www.cloudcontrol.com/sign-up?plan=Start-up http://www.cloudcontrol.com/pricing/calculator?plan=startup http://www.cloudcontrol.com/sign-up?plan=Business http://www.cloudcontrol.com/pricing/calculator?plan=business http://www.cloudcontrol.com/sign-up?plan=Business%2B http://www.cloudcontrol.com/pricing/calculator?plan=businessplus http://www.cloudcontrol.com/contact http://www.cloudcontrol.com#plantable http://www.cloudcontrol.com#plantable http://www.cloudcontrol.com/dev-center/Quickstart http://www.cloudcontrol.com/dev-center/Platform Documentation http://status.cloudcontrol.com http://www.cloudcontrol.com/dev-center/support http://www.cloudcontrol.com/console http://www.cloudcontrol.com/team http://www.cloudcontrol.com/jobs http://www.cloudcontrol.com/blog http://www.cloudcontrol.com/contact http://www.cloudcontrol.com/add-on-provider-program http://www.cloudcontrol.com/solution-provider-program http://www.whitelabelpaas.info http://www.cloudcontrol.com/tos http://www.cloudcontrol.com/privacy-policy http://www.cloudcontrol.com/imprint]
		//Images: [http://www.cloudcontrol.com/assets/spinner-6f9309f477dcc1d3c21cd21ce44dc8b2.gif]
		//Keywords: [cloudcontrol cloud control cloud hosting cloud computing cloud hosting web-hosting platform as a service paas]
		//Compatibility: map[IE:edge chrome:1]
	}
}
<commit_msg>Remove compatibility from example test to avoid fake errors<commit_after>package metainspector

import (
	"fmt"
	"net/url"
	"testing"
)

var msgFail = "%v function fails. Expects %v, returns %v"

func TestRoot(t *testing.T) {
	u, err := url.Parse("http://www.cloudcontrol.com/pricing")
	if err != nil {
		t.Errorf("%v", err)
	}
	if rootUrl := ExportRoot(u); rootUrl != "http://www.cloudcontrol.com" {
		t.Errorf(msgFail, "rootURL", "http://www.cloudcontrol.com", rootUrl)
	}
}

func TestFixURL(t *testing.T) {
	u, err := url.Parse("http://foo.bar")
	if err != nil {
		t.Errorf("%v", err)
	}
	u.Scheme, u.Host, u.Path = "", "", "www.cloudcontrol.com/pricing"
	fix := ExportFixURL(u)
	if fix.Scheme != "http" {
		t.Errorf(msgFail, "Scheme", "http", fix.Scheme)
	} else if fix.Host != "www.cloudcontrol.com" {
		t.Errorf(msgFail, "Host", "www.cloudcontrol.com", fix.Host)
	} else if fix.Path != "/pricing" {
		t.Errorf(msgFail, "Path", "/pricing", fix.Path)
	}
}

var mi = MetaInspector{url: "http://www.cloudcontrol.com",
	scheme:        "http",
	host:          "www.cloudontrol.com",
	rootUrl:       "http://www.cloudcontrol.com",
	title:         "CloudControl",
	language:      "en",
	author:        "CloudControl",
	description:   "PaaS company",
	generator:     "some generator",
	feed:          "http://www.cloudcontrol.com/feed",
	charset:       "utf-8",
	links:         []string{"http://foo.bar", "https://bar.foo"},
	images:        []string{"http://foo.jpg", "https://bar.png"},
	keywords:      []string{"cloud", "PaaS"},
	compatibility: map[string]string{"IE": "edge"},
}

func TestUrl(t *testing.T) {
	if url := mi.Url(); url != "http://www.cloudcontrol.com" {
		t.Errorf(msgFail, "Url", "www.google.com", url)
	}
}

func TestScheme(t *testing.T) {
	if scheme := mi.Scheme(); scheme != "http" {
		t.Errorf(msgFail, "Scheme", "http", scheme)
	}
}

func TestHost(t *testing.T) {
	if host := mi.Host(); host != "www.cloudontrol.com" {
		t.Errorf(msgFail, "Host", "www.cloudontrol.com", host)
	}
}

func TestRootURL(t *testing.T) {
	if rootUrl := mi.RootURL(); rootUrl != "http://www.cloudcontrol.com" {
		t.Errorf(msgFail, "RootURL", "http://www.cloudontrol.com", rootUrl)
	}
}

func TestTitle(t *testing.T) {
	if title := mi.Title(); title != "CloudControl" {
		t.Errorf(msgFail, "Title", "CloudControl", title)
	}
}

func TestLanguage(t *testing.T) {
	if language := mi.Language(); language != "en" {
		t.Errorf(msgFail, "Language", "en", language)
	}
}

func TestAuthor(t *testing.T) {
	if author := mi.Author(); author != "CloudControl" {
		t.Errorf(msgFail, "Author", "CloudControl", author)
	}
}

func TestDescription(t *testing.T) {
	if description := mi.Description(); description != "PaaS company" {
		t.Errorf(msgFail, "Description", "PaaS company", description)
	}
}

func TestGenerator(t *testing.T) {
	if generator := mi.Generator(); generator != "some generator" {
		t.Errorf(msgFail, "Generator", "some generator", generator)
	}
}

func TestFeed(t *testing.T) {
	if feed := mi.Feed(); feed != "http://www.cloudcontrol.com/feed" {
		t.Errorf(msgFail, "Feed", "http://www.cloudcontrol.com/feed", feed)
	}
}

func TestCharset(t *testing.T) {
	if charset := mi.Charset(); charset != "utf-8" {
		t.Errorf(msgFail, "Charset", "utf-8", charset)
	}
}

func TestLinks(t *testing.T) {
	l1 := fmt.Sprintf("%v", mi.Links())
	l2 := "[http://foo.bar https://bar.foo]"
	if l1 != l2 {
		t.Errorf(msgFail, "Links", l2, l1)
	}
}

func TestImages(t *testing.T) {
	i1 := fmt.Sprintf("%v", mi.Images())
	i2 := "[http://foo.jpg https://bar.png]"
	if i1 != i2 {
		t.Errorf(msgFail, "Images", i2, i1)
	}
}

func TestKeywords(t *testing.T) {
	k1 := fmt.Sprintf("%v", mi.Keywords())
	k2 := "[cloud PaaS]"
	if k1 != k2 {
		t.Errorf(msgFail, "Keywords", k2, k1)
	}
}

func TestCompatibility(t *testing.T) {
	c1 := fmt.Sprintf("%v", mi.Compatibility())
	c2 := "map[IE:edge]"
	if c1 != c2 {
		t.Errorf(msgFail, "Compatibility", c2, c1)
	}
}

func ExampleMetaInspector() {
	url := "http://www.cloudcontrol.com/pricing"
	MI, err := New(url)
	if err != nil {
		fmt.Printf("Error: %v", err)
	} else {
		fmt.Printf("\nURL: %s\n", MI.Url())
		fmt.Printf("Scheme: %s\n", MI.Scheme())
		fmt.Printf("Host: %s\n", MI.Host())
		fmt.Printf("Root: %s\n", MI.RootURL())
		fmt.Printf("Title: %s\n", MI.Title())
		fmt.Printf("Language: %s\n", MI.Language())
		fmt.Printf("Author: %s\n", MI.Author())
		fmt.Printf("Description: %s\n", MI.Description())
		fmt.Printf("Charset: %s\n", MI.Charset())
		fmt.Printf("Feed URL: %s\n", MI.Feed())
		fmt.Printf("Links: %v\n", MI.Links())
		fmt.Printf("Images: %v\n", MI.Images())
		fmt.Printf("Keywords: %v\n", MI.Keywords())
		// Output:
		//URL: http://www.cloudcontrol.com/pricing
		//Scheme: http
		//Host: www.cloudcontrol.com
		//Root: http://www.cloudcontrol.com
		//Title: cloudControl » Cloud App Platform » Pricing
		//Language: en
		//Author: cloudControl GmbH
		//Description: Cloud hosting secure, easy and fair: Highly available and scalable cloud hosting with no administraton hassle and pay as you go billing
		//Charset: utf-8
		//Feed URL: https://www.cloudcontrol.com/blog.rss
		//Links: [http://www.cloudcontrol.com/console/account/{{user.username}} http://www.cloudcontrol.com/ http://www.cloudcontrol.com/pricing http://www.cloudcontrol.com/dev-center http://www.cloudcontrol.com/add-ons http://www.cloudcontrol.com/blog http://www.cloudcontrol.com/console http://www.cloudcontrol.com/pricing/calculator http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com#included http://www.cloudcontrol.com#memoryhours http://www.cloudcontrol.com/sign-up http://www.cloudcontrol.com/pricing/calculator http://www.cloudcontrol.com/sign-up?plan=Start-up http://www.cloudcontrol.com/pricing/calculator?plan=startup http://www.cloudcontrol.com/sign-up?plan=Business http://www.cloudcontrol.com/pricing/calculator?plan=business http://www.cloudcontrol.com/sign-up?plan=Business%2B http://www.cloudcontrol.com/pricing/calculator?plan=businessplus http://www.cloudcontrol.com/contact http://www.cloudcontrol.com#plantable http://www.cloudcontrol.com#plantable http://www.cloudcontrol.com/dev-center/Quickstart http://www.cloudcontrol.com/dev-center/Platform Documentation http://status.cloudcontrol.com http://www.cloudcontrol.com/dev-center/support http://www.cloudcontrol.com/console http://www.cloudcontrol.com/team http://www.cloudcontrol.com/jobs http://www.cloudcontrol.com/blog http://www.cloudcontrol.com/contact http://www.cloudcontrol.com/add-on-provider-program http://www.cloudcontrol.com/solution-provider-program http://www.whitelabelpaas.info http://www.cloudcontrol.com/tos http://www.cloudcontrol.com/privacy-policy http://www.cloudcontrol.com/imprint]
		//Images: [http://www.cloudcontrol.com/assets/spinner-6f9309f477dcc1d3c21cd21ce44dc8b2.gif]
		//Keywords: [cloudcontrol cloud control cloud hosting cloud computing cloud hosting web-hosting platform as a service paas]
	}
}
<|endoftext|>


<commit_before>package adoc

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

// This part contains apis for the containers listed in
// https://docs.docker.com/reference/api/docker_remote_api_v1.17/#21-containers

type Port struct {
	IP          string
	PrivatePort int
	PublicPort  int
	Type        string
}

// Container defines basic container information for ListContainers
type Container struct {
	Command    string
	Created    int64
	Id         string
	Image      string
	Labels     map[string]string
	Names      []string
	Ports      []Port
	SizeRootFs int64
	SizeRw     int64
	Status     string
}

// ContainerConfig defines basic container creation data stucture
type ContainerConfig struct {
	AttachStderr    bool
	AttachStdin     bool
	AttachStdout    bool
	Cmd             []string
	CpuShares       int
	Cpuset          string
	Domainname      string
	Entrypoint      []string
	Env             []string
	ExposedPorts    map[string]struct{}
	Hostname        string
	Image           string
	Labels          map[string]string
	MacAddress      string
	Memory          int64
	MemorySwap      int64
	NetworkDisabled bool
	OnBuild         []string
	OpenStdin       bool
	PortSpecs       []string
	StdinOnce       bool
	Tty             bool
	User            string
	Volumes         map[string]struct{}
	WorkingDir      string
}

type Device struct {
	PathOnHost        string
	PathInContainer   string
	CgroupPermissions string
}

type RestartPolicy struct {
	MaximumRetryCount int
	Name              string
}

type Ulimit struct {
	Name string
	Soft int64
	Hard int64
}

type LogConfig struct {
	Type   string
	Config map[string]string
}

// HostConfig defines basic host configuration for container to run
type HostConfig struct {
	Binds           []string
	CapAdd          []string
	CapDrop         []string
	CgroupParent    string
	ContainerIDFile string
	CpuShares       int
	CpusetCpus      string
	Devices         []Device
	Dns             []string
	DnsSearch       []string
	ExtraHosts      []string
	IpcMode         string
	Links           []string
	LxcConf         []map[string]string
	Memory          int64
	MemorySwap      int64
	NetworkMode     string
	PidMode         string
	PortBindings    map[string][]PortBinding
	Privileged      bool
	PublishAllPorts bool
	ReadonlyRootfs  bool
	RestartPolicy   RestartPolicy
	SecurityOpt     []string
	VolumesFrom     []string
	Ulimits         []Ulimit  // 1.18
	LogConfig       LogConfig // 1.18
}

type PortBinding struct {
	HostIp   string
	HostPort string
}

type NetworkSettings struct {
	Bridge                 string
	Gateway                string
	GlobalIPv6Address      string
	GlobalIPv6PrefixLen    int
	IPAddress              string
	IPPrefixLen            int
	IPv6Gateway            string
	LinkLocalIPv6Address   string
	LinkLocalIPv6PrefixLen int
	MacAddress             string
	Ports                  map[string][]PortBinding
}

// ContainerState defines container running state from inspection
type ContainerState struct {
	Dead       bool
	Error      string
	ExitCode   int
	FinishedAt time.Time
	OOMKilled  bool
	Paused     bool
	Pid        int64
	Restarting bool
	Running    bool
	StartedAt  time.Time
}

// SwarmNode defines the swarm api data for container running node
type SwarmNode struct {
	Name   string
	ID     string
	Addr   string
	IP     string
	Cpus   int
	Memory int64
	Labels map[string]string
}

// ContainerDetail defines the detail data of the container from inspection, including the swarm node infor
type ContainerDetail struct {
	AppArmorProfile string
	Args            []string
	Config          ContainerConfig
	Created         time.Time
	Driver          string
	ExecDriver      string
	ExecIDs         []string
	HostConfig      HostConfig
	HostnamePath    string
	HostsPath       string
	Id              string
	Image           string
	LogPath         string
	MountLabel      string
	Name            string
	NetworkSettings NetworkSettings
	Path            string
	ProcessLabel    string
	ResolvConfPath  string
	RestartCount    int
	State           ContainerState
	Volumes         map[string]string
	VolumesRW       map[string]bool
	Node            SwarmNode // swarm api
}

// ListContainers returns containers data, showAll flag defines if you want to show all the containers including the stopped ones
func (client *DockerClient) ListContainers(showAll, showSize bool, filters ...string) ([]Container, error) {
	v := url.Values{}
	v.Set("all", formatBoolToIntString(showAll))
	v.Set("size", formatBoolToIntString(showSize))
	if len(filters) > 0 && filters[0] != "" {
		v.Set("filters", filters[0])
	}
	uri := fmt.Sprintf("containers/json?%s", v.Encode())
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return nil, err
	} else {
		var ret []Container
		err := json.Unmarshal(data, &ret)
		return ret, err
	}
}

// InspectContainer returns container detail data with container id
func (client *DockerClient) InspectContainer(id string) (ContainerDetail, error) {
	uri := fmt.Sprintf("containers/%s/json", id)
	var ret ContainerDetail
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return ret, err
	} else {
		err := json.Unmarshal(data, &ret)
		return ret, err
	}
}

func (client *DockerClient) CreateContainer(containerConf ContainerConfig, hostConf HostConfig, name ...string) (string, error) {
	var config struct {
		ContainerConfig
		HostConfig HostConfig
	}
	config.ContainerConfig = containerConf
	config.HostConfig = hostConf

	if body, err := json.Marshal(config); err != nil {
		return "", err
	} else {
		uri := "containers/create"
		if len(name) > 0 && name[0] != "" {
			v := url.Values{}
			v.Set("name", name[0])
			uri += "?" + v.Encode()
		}
		if data, err := client.sendRequest("POST", uri, body, nil, true); err != nil {
			return "", err
		} else {
			var resp struct {
				Id       string
				Warnings []string
			}
			err := json.Unmarshal(data, &resp)
			if len(resp.Warnings) > 0 {
				logger.Warnf("Create container returns warning from docker daemon: %+v", resp.Warnings)
			}
			return resp.Id, err
		}
	}
}

func (client *DockerClient) StartContainer(id string) error {
	uri := fmt.Sprintf("containers/%s/start", id)
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) StopContainer(id string, timeout ...int) error {
	uri := fmt.Sprintf("containers/%s/stop", id)
	if len(timeout) > 0 && timeout[0] >= 0 {
		v := url.Values{}
		v.Set("t", fmt.Sprintf("%d", timeout[0]))
		uri += "?" + v.Encode()
	}
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) RestartContainer(id string, timeout ...int) error {
	uri := fmt.Sprintf("containers/%s/restart", id)
	if len(timeout) > 0 && timeout[0] >= 0 {
		v := url.Values{}
		v.Set("t", fmt.Sprintf("%d", timeout[0]))
		uri += "?" + v.Encode()
	}
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) KillContainer(id string, signal ...string) error {
	uri := fmt.Sprintf("containers/%s/kill", id)
	if len(signal) > 0 && signal[0] != "" {
		v := url.Values{}
		v.Set("signal", signal[0])
		uri += "?" + v.Encode()
	}
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) PauseContainer(id string) error {
	uri := fmt.Sprintf("containers/%s/pause", id)
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) UnpauseContainer(id string) error {
	uri := fmt.Sprintf("containers/%s/unpause", id)
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) RemoveContainer(id string, force, volumes bool) error {
	v := url.Values{}
	v.Set("force", formatBoolToIntString(force))
	v.Set("v", formatBoolToIntString(volumes))
	uri := fmt.Sprintf("containers/%s?%s", id, v.Encode())
	_, err := client.sendRequest("DELETE", uri, nil, nil)
	return err
}

func (client *DockerClient) RenameContainer(id string, name string) error {
	v := url.Values{}
	v.Set("name", name)
	uri := fmt.Sprintf("containers/%s/rename?%s", id, v.Encode())
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

// This will block the call routine until the container is stopped
func (client *DockerClient) WaitContainer(id string) (int, error) {
	uri := fmt.Sprintf("containers/%s/wait", id)
	if data, err := client.sendRequest("POST", uri, nil, nil, true); err != nil {
		return 0, err
	} else {
		var ret map[string]int
		if err := json.Unmarshal(data, &ret); err != nil {
			return 0, err
		}
		if code, ok := ret["StatusCode"]; ok {
			return code, nil
		} else {
			logger.Warnf("There is no StatusCode key inside results map, the API maybe changed, ret=%+v", ret)
			return 0, fmt.Errorf("Cannot get StatusCode from return data, %+v", ret)
		}
	}
}

func (client *DockerClient) ContainerLogs(id string, stdout, stderr, timestamps bool, tail ...int) ([]LogEntry, error) {
	// no following mode
	v := url.Values{}
	v.Set("stdout", formatBoolToIntString(stdout))
	v.Set("stderr", formatBoolToIntString(stderr))
	v.Set("timestamps", formatBoolToIntString(timestamps))
	if len(tail) > 0 && tail[0] >= 0 {
		v.Set("tail", fmt.Sprintf("%d", tail[0]))
	}
	uri := fmt.Sprintf("containers/%s/logs?%s", id, v.Encode())

	var entries []LogEntry
	err := client.sendRequestCallback("GET", uri, nil, nil, func(resp *http.Response) error {
		var cbErr error
		entries, cbErr = ReadAllDockerLogs(resp.Body)
		return cbErr
	})
	return entries, err
}

type Processes struct {
	Titles    []string
	Processes [][]string
}

func (client *DockerClient) ContainerProcesses(id string, psArgs ...string) (Processes, error) {
	var procs Processes
	v := url.Values{}
	if len(psArgs) > 0 && psArgs[0] != "" {
		v.Set("ps_args", psArgs[0])
	}
	uri := fmt.Sprintf("containers/%s/top", id)
	if len(v) > 0 {
		uri += "?" + v.Encode()
	}
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return procs, err
	} else {
		err := json.Unmarshal(data, &procs)
		return procs, err
	}
}

type FsChange struct {
	Path string
	Kind int
}

func (client *DockerClient) ContainerChanges(id string) ([]FsChange, error) {
	uri := fmt.Sprintf("containers/%s/changes", id)
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return nil, err
	} else {
		var changes []FsChange
		err := json.Unmarshal(data, &changes)
		return changes, err
	}
}

// Missing apis for
// containers/(id)/copy
// containers/(id)/attach
// containers/(id)/export
// containers/(id)/resize?h=<height>&w=<width>
// containers/(id)/attach/ws
<commit_msg>Add VolumeDriver param for ContainerConfig<commit_after>package adoc

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

// This part contains apis for the containers listed in
// https://docs.docker.com/reference/api/docker_remote_api_v1.17/#21-containers

type Port struct {
	IP          string
	PrivatePort int
	PublicPort  int
	Type        string
}

// Container defines basic container information for ListContainers
type Container struct {
	Command    string
	Created    int64
	Id         string
	Image      string
	Labels     map[string]string
	Names      []string
	Ports      []Port
	SizeRootFs int64
	SizeRw     int64
	Status     string
}

// ContainerConfig defines basic container creation data stucture
type ContainerConfig struct {
	AttachStderr    bool
	AttachStdin     bool
	AttachStdout    bool
	Cmd             []string
	CpuShares       int
	Cpuset          string
	Domainname      string
	Entrypoint      []string
	Env             []string
	ExposedPorts    map[string]struct{}
	Hostname        string
	Image           string
	Labels          map[string]string
	MacAddress      string
	Memory          int64
	MemorySwap      int64
	NetworkDisabled bool
	OnBuild         []string
	OpenStdin       bool
	PortSpecs       []string
	StdinOnce       bool
	Tty             bool
	User            string
	VolumeDriver    string
	Volumes         map[string]struct{}
	WorkingDir      string
}

type Device struct {
	PathOnHost        string
	PathInContainer   string
	CgroupPermissions string
}

type RestartPolicy struct {
	MaximumRetryCount int
	Name              string
}

type Ulimit struct {
	Name string
	Soft int64
	Hard int64
}

type LogConfig struct {
	Type   string
	Config map[string]string
}

// HostConfig defines basic host configuration for container to run
type HostConfig struct {
	Binds           []string
	CapAdd          []string
	CapDrop         []string
	CgroupParent    string
	ContainerIDFile string
	CpuShares       int
	CpusetCpus      string
	Devices         []Device
	Dns             []string
	DnsSearch       []string
	ExtraHosts      []string
	IpcMode         string
	Links           []string
	LxcConf         []map[string]string
	Memory          int64
	MemorySwap      int64
	NetworkMode     string
	PidMode         string
	PortBindings    map[string][]PortBinding
	Privileged      bool
	PublishAllPorts bool
	ReadonlyRootfs  bool
	RestartPolicy   RestartPolicy
	SecurityOpt     []string
	VolumesFrom     []string
	Ulimits         []Ulimit  // 1.18
	LogConfig       LogConfig // 1.18
}

type PortBinding struct {
	HostIp   string
	HostPort string
}

type NetworkSettings struct {
	Bridge                 string
	Gateway                string
	GlobalIPv6Address      string
	GlobalIPv6PrefixLen    int
	IPAddress              string
	IPPrefixLen            int
	IPv6Gateway            string
	LinkLocalIPv6Address   string
	LinkLocalIPv6PrefixLen int
	MacAddress             string
	Ports                  map[string][]PortBinding
}

// ContainerState defines container running state from inspection
type ContainerState struct {
	Dead       bool
	Error      string
	ExitCode   int
	FinishedAt time.Time
	OOMKilled  bool
	Paused     bool
	Pid        int64
	Restarting bool
	Running    bool
	StartedAt  time.Time
}

// SwarmNode defines the swarm api data for container running node
type SwarmNode struct {
	Name   string
	ID     string
	Addr   string
	IP     string
	Cpus   int
	Memory int64
	Labels map[string]string
}

// ContainerDetail defines the detail data of the container from inspection, including the swarm node infor
type ContainerDetail struct {
	AppArmorProfile string
	Args            []string
	Config          ContainerConfig
	Created         time.Time
	Driver          string
	ExecDriver      string
	ExecIDs         []string
	HostConfig      HostConfig
	HostnamePath    string
	HostsPath       string
	Id              string
	Image           string
	LogPath         string
	MountLabel      string
	Name            string
	NetworkSettings NetworkSettings
	Path            string
	ProcessLabel    string
	ResolvConfPath  string
	RestartCount    int
	State           ContainerState
	Volumes         map[string]string
	VolumesRW       map[string]bool
	Node            SwarmNode // swarm api
}

// ListContainers returns containers data, showAll flag defines if you want to show all the containers including the stopped ones
func (client *DockerClient) ListContainers(showAll, showSize bool, filters ...string) ([]Container, error) {
	v := url.Values{}
	v.Set("all", formatBoolToIntString(showAll))
	v.Set("size", formatBoolToIntString(showSize))
	if len(filters) > 0 && filters[0] != "" {
		v.Set("filters", filters[0])
	}
	uri := fmt.Sprintf("containers/json?%s", v.Encode())
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return nil, err
	} else {
		var ret []Container
		err := json.Unmarshal(data, &ret)
		return ret, err
	}
}

// InspectContainer returns container detail data with container id
func (client *DockerClient) InspectContainer(id string) (ContainerDetail, error) {
	uri := fmt.Sprintf("containers/%s/json", id)
	var ret ContainerDetail
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return ret, err
	} else {
		err := json.Unmarshal(data, &ret)
		return ret, err
	}
}

func (client *DockerClient) CreateContainer(containerConf ContainerConfig, hostConf HostConfig, name ...string) (string, error) {
	var config struct {
		ContainerConfig
		HostConfig HostConfig
	}
	config.ContainerConfig = containerConf
	config.HostConfig = hostConf

	if body, err := json.Marshal(config); err != nil {
		return "", err
	} else {
		uri := "containers/create"
		if len(name) > 0 && name[0] != "" {
			v := url.Values{}
			v.Set("name", name[0])
			uri += "?" + v.Encode()
		}
		if data, err := client.sendRequest("POST", uri, body, nil, true); err != nil {
			return "", err
		} else {
			var resp struct {
				Id       string
				Warnings []string
			}
			err := json.Unmarshal(data, &resp)
			if len(resp.Warnings) > 0 {
				logger.Warnf("Create container returns warning from docker daemon: %+v", resp.Warnings)
			}
			return resp.Id, err
		}
	}
}

func (client *DockerClient) StartContainer(id string) error {
	uri := fmt.Sprintf("containers/%s/start", id)
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) StopContainer(id string, timeout ...int) error {
	uri := fmt.Sprintf("containers/%s/stop", id)
	if len(timeout) > 0 && timeout[0] >= 0 {
		v := url.Values{}
		v.Set("t", fmt.Sprintf("%d", timeout[0]))
		uri += "?" + v.Encode()
	}
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) RestartContainer(id string, timeout ...int) error {
	uri := fmt.Sprintf("containers/%s/restart", id)
	if len(timeout) > 0 && timeout[0] >= 0 {
		v := url.Values{}
		v.Set("t", fmt.Sprintf("%d", timeout[0]))
		uri += "?" + v.Encode()
	}
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) KillContainer(id string, signal ...string) error {
	uri := fmt.Sprintf("containers/%s/kill", id)
	if len(signal) > 0 && signal[0] != "" {
		v := url.Values{}
		v.Set("signal", signal[0])
		uri += "?" + v.Encode()
	}
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) PauseContainer(id string) error {
	uri := fmt.Sprintf("containers/%s/pause", id)
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) UnpauseContainer(id string) error {
	uri := fmt.Sprintf("containers/%s/unpause", id)
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

func (client *DockerClient) RemoveContainer(id string, force, volumes bool) error {
	v := url.Values{}
	v.Set("force", formatBoolToIntString(force))
	v.Set("v", formatBoolToIntString(volumes))
	uri := fmt.Sprintf("containers/%s?%s", id, v.Encode())
	_, err := client.sendRequest("DELETE", uri, nil, nil)
	return err
}

func (client *DockerClient) RenameContainer(id string, name string) error {
	v := url.Values{}
	v.Set("name", name)
	uri := fmt.Sprintf("containers/%s/rename?%s", id, v.Encode())
	_, err := client.sendRequest("POST", uri, nil, nil)
	return err
}

// This will block the call routine until the container is stopped
func (client *DockerClient) WaitContainer(id string) (int, error) {
	uri := fmt.Sprintf("containers/%s/wait", id)
	if data, err := client.sendRequest("POST", uri, nil, nil, true); err != nil {
		return 0, err
	} else {
		var ret map[string]int
		if err := json.Unmarshal(data, &ret); err != nil {
			return 0, err
		}
		if code, ok := ret["StatusCode"]; ok {
			return code, nil
		} else {
			logger.Warnf("There is no StatusCode key inside results map, the API maybe changed, ret=%+v", ret)
			return 0, fmt.Errorf("Cannot get StatusCode from return data, %+v", ret)
		}
	}
}

func (client *DockerClient) ContainerLogs(id string, stdout, stderr, timestamps bool, tail ...int) ([]LogEntry, error) {
	// no following mode
	v := url.Values{}
	v.Set("stdout", formatBoolToIntString(stdout))
	v.Set("stderr", formatBoolToIntString(stderr))
	v.Set("timestamps", formatBoolToIntString(timestamps))
	if len(tail) > 0 && tail[0] >= 0 {
		v.Set("tail", fmt.Sprintf("%d", tail[0]))
	}
	uri := fmt.Sprintf("containers/%s/logs?%s", id, v.Encode())

	var entries []LogEntry
	err := client.sendRequestCallback("GET", uri, nil, nil, func(resp *http.Response) error {
		var cbErr error
		entries, cbErr = ReadAllDockerLogs(resp.Body)
		return cbErr
	})
	return entries, err
}

type Processes struct {
	Titles    []string
	Processes [][]string
}

func (client *DockerClient) ContainerProcesses(id string, psArgs ...string) (Processes, error) {
	var procs Processes
	v := url.Values{}
	if len(psArgs) > 0 && psArgs[0] != "" {
		v.Set("ps_args", psArgs[0])
	}
	uri := fmt.Sprintf("containers/%s/top", id)
	if len(v) > 0 {
		uri += "?" + v.Encode()
	}
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return procs, err
	} else {
		err := json.Unmarshal(data, &procs)
		return procs, err
	}
}

type FsChange struct {
	Path string
	Kind int
}

func (client *DockerClient) ContainerChanges(id string) ([]FsChange, error) {
	uri := fmt.Sprintf("containers/%s/changes", id)
	if data, err := client.sendRequest("GET", uri, nil, nil); err != nil {
		return nil, err
	} else {
		var changes []FsChange
		err := json.Unmarshal(data, &changes)
		return changes, err
	}
}

// Missing apis for
// containers/(id)/copy
// containers/(id)/attach
// containers/(id)/export
// containers/(id)/resize?h=<height>&w=<width>
// containers/(id)/attach/ws
<|endoftext|>