<commit_before>// Copyright Project Harbor 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 image

import (
	"errors"
	"fmt"
	"strings"

	"github.com/docker/distribution/manifest/manifestlist"

	"github.com/docker/distribution"
	"github.com/docker/distribution/manifest/schema1"
	"github.com/docker/distribution/manifest/schema2"
	"github.com/goharbor/harbor/src/common/utils/log"
	"github.com/goharbor/harbor/src/replication/adapter"
	"github.com/goharbor/harbor/src/replication/model"
	trans "github.com/goharbor/harbor/src/replication/transfer"
)

func init() {
	if err := trans.RegisterFactory(model.ResourceTypeImage, factory); err != nil {
		log.Errorf("failed to register transfer factory: %v", err)
	}
}

type repository struct {
	repository string
	tags       []string
}

func factory(logger trans.Logger, stopFunc trans.StopFunc) (trans.Transfer, error) {
	return &transfer{
		logger:    logger,
		isStopped: stopFunc,
	}, nil
}

type transfer struct {
	logger    trans.Logger
	isStopped trans.StopFunc
	src       adapter.ImageRegistry
	dst       adapter.ImageRegistry
}

func (t *transfer) Transfer(src *model.Resource, dst *model.Resource) error {
	// initialize
	if err := t.initialize(src, dst); err != nil {
		return err
	}

	// delete the repository on destination registry
	if dst.Deleted {
		return t.delete(&repository{
			repository: dst.Metadata.GetResourceName(),
			tags:       dst.Metadata.Vtags,
		})
	}

	srcRepo := &repository{
		repository: src.Metadata.GetResourceName(),
		tags:       src.Metadata.Vtags,
	}
	dstRepo := &repository{
		repository: dst.Metadata.GetResourceName(),
		tags:       dst.Metadata.Vtags,
	}
	// copy the repository from source registry to the destination
	return t.copy(srcRepo, dstRepo, dst.Override)
}

func (t *transfer) initialize(src *model.Resource, dst *model.Resource) error {
	if t.shouldStop() {
		return nil
	}
	// create client for source registry
	srcReg, err := createRegistry(src.Registry)
	if err != nil {
		t.logger.Errorf("failed to create client for source registry: %v", err)
		return err
	}
	t.src = srcReg
	t.logger.Infof("client for source registry [type: %s, URL: %s, insecure: %v] created",
		src.Registry.Type, src.Registry.URL, src.Registry.Insecure)

	// create client for destination registry
	dstReg, err := createRegistry(dst.Registry)
	if err != nil {
		t.logger.Errorf("failed to create client for destination registry: %v", err)
		return err
	}
	t.dst = dstReg
	t.logger.Infof("client for destination registry [type: %s, URL: %s, insecure: %v] created",
		dst.Registry.Type, dst.Registry.URL, dst.Registry.Insecure)

	return nil
}

func createRegistry(reg *model.Registry) (adapter.ImageRegistry, error) {
	factory, err := adapter.GetFactory(reg.Type)
	if err != nil {
		return nil, err
	}
	ad, err := factory.Create(reg)
	if err != nil {
		return nil, err
	}
	registry, ok := ad.(adapter.ImageRegistry)
	if !ok {
		return nil, errors.New("the adapter doesn't implement the \"ImageRegistry\" interface")
	}
	return registry, nil
}

func (t *transfer) shouldStop() bool {
	isStopped := t.isStopped()
	if isStopped {
		t.logger.Info("the job is stopped")
	}
	return isStopped
}

func (t *transfer) copy(src *repository, dst *repository, override bool) error {
	srcRepo := src.repository
	dstRepo := dst.repository
	t.logger.Infof("copying %s:[%s](source registry) to %s:[%s](destination registry)...",
		srcRepo, strings.Join(src.tags, ","), dstRepo, strings.Join(dst.tags, ","))
	var err error
	for i := range src.tags {
		if e := t.copyImage(srcRepo, src.tags[i], dstRepo, dst.tags[i], override); e != nil {
			t.logger.Errorf(e.Error())
			err = e
		}
	}
	if err != nil {
		return err
	}

	t.logger.Infof("copy %s:[%s](source registry) to %s:[%s](destination registry) completed",
		srcRepo, strings.Join(src.tags, ","), dstRepo, strings.Join(dst.tags, ","))
	return nil
}

func (t *transfer) copyImage(srcRepo, srcRef, dstRepo, dstRef string, override bool) error {
	t.logger.Infof("copying %s:%s(source registry) to %s:%s(destination registry)...",
		srcRepo, srcRef, dstRepo, dstRef)
	// pull the manifest from the source registry
	manifest, digest, err := t.pullManifest(srcRepo, srcRef)
	if err != nil {
		return err
	}

	// check the existence of the image on the destination registry
	exist, digest2, err := t.exist(dstRepo, dstRef)
	if err != nil {
		return err
	}
	if exist {
		// the same image already exists
		if digest == digest2 {
			t.logger.Infof("the image %s:%s already exists on the destination registry, skip",
				dstRepo, dstRef)
			return nil
		}
		// the same name image exists, but not allowed to override
		if !override {
			t.logger.Warningf("the same name image %s:%s exists on the destination registry, but the \"override\" is set to false, skip",
				dstRepo, dstRef)
			return nil
		}
		// the same name image exists, but allowed to override
		t.logger.Warningf("the same name image %s:%s exists on the destination registry and the \"override\" is set to true, continue...",
			dstRepo, dstRef)
	}

	// copy contents between the source and destination registries
	for _, content := range manifest.References() {
		if err = t.copyContent(content, srcRepo, dstRepo); err != nil {
			return err
		}
	}

	// push the manifest to the destination registry
	if err := t.pushManifest(manifest, dstRepo, dstRef); err != nil {
		return err
	}

	t.logger.Infof("copy %s:%s(source registry) to %s:%s(destination registry) completed",
		srcRepo, srcRef, dstRepo, dstRef)
	return nil
}

// copy the content from source registry to destination according to its media type
func (t *transfer) copyContent(content distribution.Descriptor, srcRepo, dstRepo string) error {
	digest := content.Digest.String()
	switch content.MediaType {
	// when the media type of pulled manifest is manifest list,
	// the contents it contains are a few manifests
	case schema2.MediaTypeManifest:
		// as using digest as the reference, so set the override to true directly
		return t.copyImage(srcRepo, digest, dstRepo, digest, true)
	// handle foreign layer
	case schema2.MediaTypeForeignLayer:
		t.logger.Infof("the layer %s is a foreign layer, skip", digest)
		return nil
	// copy layer or image config
	// the media type of the layer or config can be "application/octet-stream",
	// schema1.MediaTypeManifestLayer, schema2.MediaTypeLayer, schema2.MediaTypeImageConfig
	default:
		return t.copyBlob(srcRepo, dstRepo, digest)
	}
}

// copy the layer or image config from the source registry to destination
func (t *transfer) copyBlob(srcRepo, dstRepo, digest string) error {
	if t.shouldStop() {
		return nil
	}
	t.logger.Infof("copying the blob %s...", digest)
	exist, err := t.dst.BlobExist(dstRepo, digest)
	if err != nil {
		t.logger.Errorf("failed to check the existence of blob %s on the destination registry: %v", digest, err)
		return err
	}
	if exist {
		t.logger.Infof("the blob %s already exists on the destination registry, skip", digest)
		return nil
	}

	size, data, err := t.src.PullBlob(srcRepo, digest)
	if err != nil {
		t.logger.Errorf("failed to pulling the blob %s: %v", digest, err)
		return err
	}
	defer data.Close()
	if err = t.dst.PushBlob(dstRepo, digest, size, data); err != nil {
		t.logger.Errorf("failed to pushing the blob %s: %v", digest, err)
		return err
	}
	t.logger.Infof("copy the blob %s completed", digest)
	return nil
}

func (t *transfer) pullManifest(repository, reference string) (
	distribution.Manifest, string, error) {
	if t.shouldStop() {
		return nil, "", nil
	}
	t.logger.Infof("pulling the manifest of image %s:%s ...", repository, reference)
	manifest, digest, err := t.src.PullManifest(repository, reference, []string{
		schema1.MediaTypeManifest,
		schema1.MediaTypeSignedManifest,
		schema2.MediaTypeManifest,
		manifestlist.MediaTypeManifestList,
	})
	if err != nil {
		t.logger.Errorf("failed to pull the manifest of image %s:%s: %v", repository, reference, err)
		return nil, "", err
	}
	t.logger.Infof("the manifest of image %s:%s pulled", repository, reference)

	// this is a solution to work around that harbor doesn't support manifest list
	return t.handleManifest(manifest, repository, digest)
}

// if the media type of the specified manifest is manifest list, just abstract one
// manifest from the list and return it
func (t *transfer) handleManifest(manifest distribution.Manifest, repository, digest string) (
	distribution.Manifest, string, error) {
	mediaType, _, err := manifest.Payload()
	if err != nil {
		t.logger.Errorf("failed to call the payload method for manifest of %s:%s: %v", repository, digest, err)
		return nil, "", err
	}
	// manifest
	if mediaType == schema1.MediaTypeManifest ||
		mediaType == schema1.MediaTypeSignedManifest ||
		mediaType == schema2.MediaTypeManifest {
		return manifest, digest, nil
	}
	// manifest list
	t.logger.Info("trying abstract a manifest from the manifest list...")
	manifestlist, ok := manifest.(*manifestlist.DeserializedManifestList)
	if !ok {
		err := fmt.Errorf("the object isn't a DeserializedManifestList")
		t.logger.Errorf(err.Error())
		return nil, "", err
	}
	digest = ""
	for _, reference := range manifestlist.Manifests {
		if strings.ToLower(reference.Platform.Architecture) == "amd64" &&
			strings.ToLower(reference.Platform.OS) == "linux" {
			digest = reference.Digest.String()
			t.logger.Infof("a manifest(architecture: amd64, os: linux) found, using this one: %s", digest)
			break
		}
	}
	if len(digest) == 0 {
		digest = manifest.References()[0].Digest.String()
		t.logger.Infof("no manifest(architecture: amd64, os: linux) found, using the first one: %s", digest)
	}
	return t.pullManifest(repository, digest)
}

func (t *transfer) exist(repository, tag string) (bool, string, error) {
	exist, digest, err := t.dst.ManifestExist(repository, tag)
	if err != nil {
		t.logger.Errorf("failed to check the existence of the manifest of image %s:%s on the destination registry: %v",
			repository, tag, err)
		return false, "", err
	}
	return exist, digest, nil
}

func (t *transfer) pushManifest(manifest distribution.Manifest, repository, tag string) error {
	if t.shouldStop() {
		return nil
	}
	t.logger.Infof("pushing the manifest of image %s:%s ...", repository, tag)
	mediaType, payload, err := manifest.Payload()
	if err != nil {
		t.logger.Errorf("failed to push manifest of image %s:%s: %v",
			repository, tag, err)
		return err
	}
	if err := t.dst.PushManifest(repository, tag, mediaType, payload); err != nil {
		t.logger.Errorf("failed to push manifest of image %s:%s: %v",
			repository, tag, err)
		return err
	}
	t.logger.Infof("the manifest of image %s:%s pushed",
		repository, tag)
	return nil
}

func (t *transfer) delete(repo *repository) error {
	if t.shouldStop() {
		return nil
	}

	repository := repo.repository
	for _, tag := range repo.tags {
		exist, _, err := t.dst.ManifestExist(repository, tag)
		if err != nil {
			t.logger.Errorf("failed to check the existence of the manifest of image %s:%s on the destination registry: %v",
				repository, tag, err)
			return err
		}
		if !exist {
			t.logger.Infof("the image %s:%s doesn't exist on the destination registry, skip",
				repository, tag)
			continue
		}
		if err := t.dst.DeleteManifest(repository, tag); err != nil {
			t.logger.Errorf("failed to delete the manifest of image %s:%s on the destination registry: %v",
				repository, tag, err)
			return err
		}
		t.logger.Infof("the manifest of image %s:%s is deleted", repository, tag)
	}
	return nil
}
<commit_msg>Remove the workaround for blocking manifest list in replication<commit_after>// Copyright Project Harbor 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 image

import (
	"errors"
	"strings"

	"github.com/docker/distribution/manifest/manifestlist"

	"github.com/docker/distribution"
	"github.com/docker/distribution/manifest/schema1"
	"github.com/docker/distribution/manifest/schema2"
	"github.com/goharbor/harbor/src/common/utils/log"
	"github.com/goharbor/harbor/src/replication/adapter"
	"github.com/goharbor/harbor/src/replication/model"
	trans "github.com/goharbor/harbor/src/replication/transfer"
)

func init() {
	if err := trans.RegisterFactory(model.ResourceTypeImage, factory); err != nil {
		log.Errorf("failed to register transfer factory: %v", err)
	}
}

type repository struct {
	repository string
	tags       []string
}

func factory(logger trans.Logger, stopFunc trans.StopFunc) (trans.Transfer, error) {
	return &transfer{
		logger:    logger,
		isStopped: stopFunc,
	}, nil
}

type transfer struct {
	logger    trans.Logger
	isStopped trans.StopFunc
	src       adapter.ImageRegistry
	dst       adapter.ImageRegistry
}

func (t *transfer) Transfer(src *model.Resource, dst *model.Resource) error {
	// initialize
	if err := t.initialize(src, dst); err != nil {
		return err
	}

	// delete the repository on destination registry
	if dst.Deleted {
		return t.delete(&repository{
			repository: dst.Metadata.GetResourceName(),
			tags:       dst.Metadata.Vtags,
		})
	}

	srcRepo := &repository{
		repository: src.Metadata.GetResourceName(),
		tags:       src.Metadata.Vtags,
	}
	dstRepo := &repository{
		repository: dst.Metadata.GetResourceName(),
		tags:       dst.Metadata.Vtags,
	}
	// copy the repository from source registry to the destination
	return t.copy(srcRepo, dstRepo, dst.Override)
}

func (t *transfer) initialize(src *model.Resource, dst *model.Resource) error {
	if t.shouldStop() {
		return nil
	}
	// create client for source registry
	srcReg, err := createRegistry(src.Registry)
	if err != nil {
		t.logger.Errorf("failed to create client for source registry: %v", err)
		return err
	}
	t.src = srcReg
	t.logger.Infof("client for source registry [type: %s, URL: %s, insecure: %v] created",
		src.Registry.Type, src.Registry.URL, src.Registry.Insecure)

	// create client for destination registry
	dstReg, err := createRegistry(dst.Registry)
	if err != nil {
		t.logger.Errorf("failed to create client for destination registry: %v", err)
		return err
	}
	t.dst = dstReg
	t.logger.Infof("client for destination registry [type: %s, URL: %s, insecure: %v] created",
		dst.Registry.Type, dst.Registry.URL, dst.Registry.Insecure)

	return nil
}

func createRegistry(reg *model.Registry) (adapter.ImageRegistry, error) {
	factory, err := adapter.GetFactory(reg.Type)
	if err != nil {
		return nil, err
	}
	ad, err := factory.Create(reg)
	if err != nil {
		return nil, err
	}
	registry, ok := ad.(adapter.ImageRegistry)
	if !ok {
		return nil, errors.New("the adapter doesn't implement the \"ImageRegistry\" interface")
	}
	return registry, nil
}

func (t *transfer) shouldStop() bool {
	isStopped := t.isStopped()
	if isStopped {
		t.logger.Info("the job is stopped")
	}
	return isStopped
}

func (t *transfer) copy(src *repository, dst *repository, override bool) error {
	srcRepo := src.repository
	dstRepo := dst.repository
	t.logger.Infof("copying %s:[%s](source registry) to %s:[%s](destination registry)...",
		srcRepo, strings.Join(src.tags, ","), dstRepo, strings.Join(dst.tags, ","))
	var err error
	for i := range src.tags {
		if e := t.copyImage(srcRepo, src.tags[i], dstRepo, dst.tags[i], override); e != nil {
			t.logger.Errorf(e.Error())
			err = e
		}
	}
	if err != nil {
		return err
	}

	t.logger.Infof("copy %s:[%s](source registry) to %s:[%s](destination registry) completed",
		srcRepo, strings.Join(src.tags, ","), dstRepo, strings.Join(dst.tags, ","))
	return nil
}

func (t *transfer) copyImage(srcRepo, srcRef, dstRepo, dstRef string, override bool) error {
	t.logger.Infof("copying %s:%s(source registry) to %s:%s(destination registry)...",
		srcRepo, srcRef, dstRepo, dstRef)
	// pull the manifest from the source registry
	manifest, digest, err := t.pullManifest(srcRepo, srcRef)
	if err != nil {
		return err
	}

	// check the existence of the image on the destination registry
	exist, digest2, err := t.exist(dstRepo, dstRef)
	if err != nil {
		return err
	}
	if exist {
		// the same image already exists
		if digest == digest2 {
			t.logger.Infof("the image %s:%s already exists on the destination registry, skip",
				dstRepo, dstRef)
			return nil
		}
		// the same name image exists, but not allowed to override
		if !override {
			t.logger.Warningf("the same name image %s:%s exists on the destination registry, but the \"override\" is set to false, skip",
				dstRepo, dstRef)
			return nil
		}
		// the same name image exists, but allowed to override
		t.logger.Warningf("the same name image %s:%s exists on the destination registry and the \"override\" is set to true, continue...",
			dstRepo, dstRef)
	}

	// copy contents between the source and destination registries
	for _, content := range manifest.References() {
		if err = t.copyContent(content, srcRepo, dstRepo); err != nil {
			return err
		}
	}

	// push the manifest to the destination registry
	if err := t.pushManifest(manifest, dstRepo, dstRef); err != nil {
		return err
	}

	t.logger.Infof("copy %s:%s(source registry) to %s:%s(destination registry) completed",
		srcRepo, srcRef, dstRepo, dstRef)
	return nil
}

// copy the content from source registry to destination according to its media type
func (t *transfer) copyContent(content distribution.Descriptor, srcRepo, dstRepo string) error {
	digest := content.Digest.String()
	switch content.MediaType {
	// when the media type of pulled manifest is manifest list,
	// the contents it contains are a few manifests
	case schema2.MediaTypeManifest:
		// as using digest as the reference, so set the override to true directly
		return t.copyImage(srcRepo, digest, dstRepo, digest, true)
	// handle foreign layer
	case schema2.MediaTypeForeignLayer:
		t.logger.Infof("the layer %s is a foreign layer, skip", digest)
		return nil
	// copy layer or image config
	// the media type of the layer or config can be "application/octet-stream",
	// schema1.MediaTypeManifestLayer, schema2.MediaTypeLayer, schema2.MediaTypeImageConfig
	default:
		return t.copyBlob(srcRepo, dstRepo, digest)
	}
}

// copy the layer or image config from the source registry to destination
func (t *transfer) copyBlob(srcRepo, dstRepo, digest string) error {
	if t.shouldStop() {
		return nil
	}
	t.logger.Infof("copying the blob %s...", digest)
	exist, err := t.dst.BlobExist(dstRepo, digest)
	if err != nil {
		t.logger.Errorf("failed to check the existence of blob %s on the destination registry: %v", digest, err)
		return err
	}
	if exist {
		t.logger.Infof("the blob %s already exists on the destination registry, skip", digest)
		return nil
	}

	size, data, err := t.src.PullBlob(srcRepo, digest)
	if err != nil {
		t.logger.Errorf("failed to pulling the blob %s: %v", digest, err)
		return err
	}
	defer data.Close()
	if err = t.dst.PushBlob(dstRepo, digest, size, data); err != nil {
		t.logger.Errorf("failed to pushing the blob %s: %v", digest, err)
		return err
	}
	t.logger.Infof("copy the blob %s completed", digest)
	return nil
}

func (t *transfer) pullManifest(repository, reference string) (
	distribution.Manifest, string, error) {
	if t.shouldStop() {
		return nil, "", nil
	}
	t.logger.Infof("pulling the manifest of image %s:%s ...", repository, reference)
	// TODO add OCI media types
	manifest, digest, err := t.src.PullManifest(repository, reference, []string{
		schema1.MediaTypeManifest,
		schema1.MediaTypeSignedManifest,
		schema2.MediaTypeManifest,
		manifestlist.MediaTypeManifestList,
	})
	if err != nil {
		t.logger.Errorf("failed to pull the manifest of image %s:%s: %v", repository, reference, err)
		return nil, "", err
	}
	t.logger.Infof("the manifest of image %s:%s pulled", repository, reference)

	return manifest, digest, nil
}

func (t *transfer) exist(repository, tag string) (bool, string, error) {
	exist, digest, err := t.dst.ManifestExist(repository, tag)
	if err != nil {
		t.logger.Errorf("failed to check the existence of the manifest of image %s:%s on the destination registry: %v",
			repository, tag, err)
		return false, "", err
	}
	return exist, digest, nil
}

func (t *transfer) pushManifest(manifest distribution.Manifest, repository, tag string) error {
	if t.shouldStop() {
		return nil
	}
	t.logger.Infof("pushing the manifest of image %s:%s ...", repository, tag)
	mediaType, payload, err := manifest.Payload()
	if err != nil {
		t.logger.Errorf("failed to push manifest of image %s:%s: %v",
			repository, tag, err)
		return err
	}
	if err := t.dst.PushManifest(repository, tag, mediaType, payload); err != nil {
		t.logger.Errorf("failed to push manifest of image %s:%s: %v",
			repository, tag, err)
		return err
	}
	t.logger.Infof("the manifest of image %s:%s pushed",
		repository, tag)
	return nil
}

func (t *transfer) delete(repo *repository) error {
	if t.shouldStop() {
		return nil
	}

	repository := repo.repository
	for _, tag := range repo.tags {
		exist, _, err := t.dst.ManifestExist(repository, tag)
		if err != nil {
			t.logger.Errorf("failed to check the existence of the manifest of image %s:%s on the destination registry: %v",
				repository, tag, err)
			return err
		}
		if !exist {
			t.logger.Infof("the image %s:%s doesn't exist on the destination registry, skip",
				repository, tag)
			continue
		}
		if err := t.dst.DeleteManifest(repository, tag); err != nil {
			t.logger.Errorf("failed to delete the manifest of image %s:%s on the destination registry: %v",
				repository, tag, err)
			return err
		}
		t.logger.Infof("the manifest of image %s:%s is deleted", repository, tag)
	}
	return nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"log"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/mmcdole/gofeed"
)

var (
	cutoff     time.Time
	titleRegex = regexp.MustCompile(
		`(?i)\[horriblesubs\] (.+) - ([0-9]{1,4}) \[(1080p|720p|480p)\]`)
	rssURL = "http://horriblesubs.info/rss.php?res=sd"
)

func rssReader() {
	defer rssReaderCleanup()

	// Parse the RSS URL
	fp := gofeed.NewParser()
	feed, err := fp.ParseURL(rssURL)
	if err != nil {
		panic(fmt.Sprintf("Error trying to parse RSS feed URL: %s - %s", rssURL, err))
	}

	// Updated titles
	var titleUpdates []string

	//Iterates through the RSS feed items in reverse order
	for i := len(feed.Items) - 1; i >= 0; i-- {
		//True if the publish time/date of this feed item is after the cutoff time/date
		relevantDate := feed.Items[i].PublishedParsed.After(cutoff)
		//True if the title of this feed item matches the regular expression
		relevantTitle := titleRegex.MatchString(feed.Items[i].Title)

		// If there is a new RSS entry published since last update date
		// handle it with newUpdate() function
		// update cutoff time with the latest update time
		if relevantTitle && relevantDate {
			regexArray := titleRegex.FindStringSubmatch(feed.Items[i].Title)
			ok := newUpdate(regexArray)

			if ok {
				titleUpdates = appendUnique(titleUpdates, regexArray[1])
				cutoff = *feed.Items[i].PublishedParsed
			}
		}
	}

	if len(titleUpdates) > 0 {
		log.Printf("Updated %d anime entries: %s", len(titleUpdates), strings.Join(titleUpdates, ", "))
	}
}

// rssReaderCleanup
// will recover from any panics during RSS URL parsing
func rssReaderCleanup() {
	if r := recover(); r != nil {
		log.Println(r)
	}
}

// new episode or series handler function
// args
// 0 = full message
// 1 = anime name
// 2 = episode (1-4 length integers only)
// 3 = resolution (1080p | 720p | 480p)
func newUpdate(args []string) bool {
	epnum, _ := strconv.Atoi(args[2])
	entry := anime{Name: args[1], Episode: epnum}

	if entry.Exists() && entry.NewEpisode() {
		// If this series already exists in the database
		// but needs to update the episode number
		entry.UpdateEp()

		if len(entry.Subs) > 0 {
			newMessage := fmt.Sprintf(
				"**New episode of %s released - Episode %d**\n",
				entry.Name, entry.Episode)

			// Add mentions for every subbed user
			for _, person := range entry.Subs {
				newMessage += fmt.Sprintf("<@%s>", person)
			}

			// Add downloads link
			if entry.Href != "" {
				newMessage += fmt.Sprintf("\nDownload at %s\n", entry.Href)
			} else {
				newMessage += fmt.Sprint("\nDownload at http://horriblesubs.info/\n")
			}

			// Add subscribe ID
			newMessage += fmt.Sprintf(
				"To subscribe to this anime type \"!sub %s\"",
				entry.ID)

			// Send update message to all anime channels
			// TODO: make a function
			for _, channel := range discordCfg.AnimeChannels {
				discord.ChannelMessageSend(channel, newMessage)
			}
		}

		return true
	} else if !entry.Exists() {
		// If this series does not exist in the database yet
		// Insert it into the database
		entry.Insert()

		// Announce new series to all anime channels
		newMessage := fmt.Sprintf("**New series started: %s - Episode %d**\n",
			entry.Name, entry.Episode)
		newMessage += fmt.Sprintf("To subscribe to this anime type \"!sub %s\"",
			entry.ID)

		// TODO: make a function
		for _, channel := range discordCfg.AnimeChannels {
			discord.ChannelMessageSend(channel, newMessage)
		}

		return true
	}

	return false
}
<commit_msg>Add logging for events where new feed item doesn't match the regex<commit_after>package main

import (
	"fmt"
	"log"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/mmcdole/gofeed"
)

var (
	cutoff     time.Time
	titleRegex = regexp.MustCompile(
		`(?i)\[horriblesubs\] (.+) - ([0-9]{1,4}) \[(1080p|720p|480p)\]`)
	rssURL = "http://horriblesubs.info/rss.php?res=sd"
)

func rssReader() {
	defer rssReaderCleanup()

	// Parse the RSS URL
	fp := gofeed.NewParser()
	feed, err := fp.ParseURL(rssURL)
	if err != nil {
		panic(fmt.Sprintf("Error trying to parse RSS feed URL: %s - %s", rssURL, err))
	}

	// Updated titles
	var titleUpdates []string

	//Iterates through the RSS feed items in reverse order
	for i := len(feed.Items) - 1; i >= 0; i-- {
		//True if the publish time/date of this feed item is after the cutoff time/date
		relevantDate := feed.Items[i].PublishedParsed.After(cutoff)
		//True if the title of this feed item matches the regular expression
		relevantTitle := titleRegex.MatchString(feed.Items[i].Title)

		// If there is a new RSS entry published since last update date
		// handle it with newUpdate() function
		// update cutoff time with the latest update time
		if relevantTitle && relevantDate {
			regexArray := titleRegex.FindStringSubmatch(feed.Items[i].Title)
			ok := newUpdate(regexArray)

			if ok {
				titleUpdates = appendUnique(titleUpdates, regexArray[1])
				cutoff = *feed.Items[i].PublishedParsed
			}
		} else if relevantDate && !relevantTitle {
			log.Println("Error trying to match this RSS feed item title:", feed.Items[i].Title)
		}
	}

	if len(titleUpdates) > 0 {
		log.Printf("Updated %d anime entries: %s", len(titleUpdates), strings.Join(titleUpdates, ", "))
	}
}

// rssReaderCleanup
// will recover from any panics during RSS URL parsing
func rssReaderCleanup() {
	if r := recover(); r != nil {
		log.Println(r)
	}
}

// new episode or series handler function
// args
// 0 = full message
// 1 = anime name
// 2 = episode (1-4 length integers only)
// 3 = resolution (1080p | 720p | 480p)
func newUpdate(args []string) bool {
	epnum, _ := strconv.Atoi(args[2])
	entry := anime{Name: args[1], Episode: epnum}

	if entry.Exists() && entry.NewEpisode() {
		// If this series already exists in the database
		// but needs to update the episode number
		entry.UpdateEp()

		if len(entry.Subs) > 0 {
			newMessage := fmt.Sprintf(
				"**New episode of %s released - Episode %d**\n",
				entry.Name, entry.Episode)

			// Add mentions for every subbed user
			for _, person := range entry.Subs {
				newMessage += fmt.Sprintf("<@%s>", person)
			}

			// Add downloads link
			if entry.Href != "" {
				newMessage += fmt.Sprintf("\nDownload at %s\n", entry.Href)
			} else {
				newMessage += fmt.Sprint("\nDownload at http://horriblesubs.info/\n")
			}

			// Add subscribe ID
			newMessage += fmt.Sprintf(
				"To subscribe to this anime type \"!sub %s\"",
				entry.ID)

			// Send update message to all anime channels
			// TODO: make a function
			for _, channel := range discordCfg.AnimeChannels {
				discord.ChannelMessageSend(channel, newMessage)
			}
		}

		return true
	} else if !entry.Exists() {
		// If this series does not exist in the database yet
		// Insert it into the database
		entry.Insert()

		// Announce new series to all anime channels
		newMessage := fmt.Sprintf("**New series started: %s - Episode %d**\n",
			entry.Name, entry.Episode)
		newMessage += fmt.Sprintf("To subscribe to this anime type \"!sub %s\"",
			entry.ID)

		// TODO: make a function
		for _, channel := range discordCfg.AnimeChannels {
			discord.ChannelMessageSend(channel, newMessage)
		}

		return true
	}

	return false
}
<|endoftext|>


<commit_before>// Copyright 2019 The LUCI 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 main

import (
	"fmt"
	"sort"
	"strings"

	"github.com/golang/protobuf/proto"
	"github.com/maruel/subcommands"
	"google.golang.org/grpc/codes"

	"go.chromium.org/luci/auth"
	"go.chromium.org/luci/common/cli"

	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
)

func cmdLS(defaultAuthOpts auth.Options) *subcommands.Command {
	return &subcommands.Command{
		UsageLine: `ls [flags] <PATH> [<PATH>...]`,
		ShortDesc: "lists builds under paths",
		LongDesc: `Lists builds under paths.

A PATH can be one of
- "<project>"
- "<project>/<bucket>"
- "<project>/<bucket>/<builder>"

Listed builds are sorted by creation time, descending.
`,
		CommandRun: func() subcommands.CommandRun {
			r := &lsRun{}
			r.RegisterGlobalFlags(defaultAuthOpts)
			r.buildFieldFlags.Register(&r.Flags)
			return r
		},
	}
}

type lsRun struct {
	baseCommandRun
	buildFieldFlags
}

func (r *lsRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
	ctx := cli.GetContext(a, r, env)
	if err := r.initClients(ctx); err != nil {
		return r.done(ctx, err)
	}

	req, err := r.parseSearchRequests(args)
	if err != nil {
		return r.done(ctx, err)
	}

	res, err := r.client.Batch(ctx, req)
	if err != nil {
		return r.done(ctx, err)
	}

	seen := map[int64]struct{}{}
	var builds []*buildbucketpb.Build
	for _, res := range res.Responses {
		if err := res.GetError(); err != nil {
			return r.done(ctx, fmt.Errorf("%s: %s", codes.Code(err.Code), err.Message))
		}
		for _, b := range res.GetSearchBuilds().Builds {
			if _, ok := seen[b.Id]; ok {
				continue
			}
			seen[b.Id] = struct{}{}
			builds = append(builds, b)
		}
	}
	// Sort by creation time, descending.
	// Build IDs are monotonically decreasing.
	sort.Slice(builds, func(i, j int) bool { return builds[i].Id < builds[j].Id })

	p := newStdoutPrinter(r.noColor)
	for _, b := range builds {
		if r.json {
			p.JSONPB(b)
		} else {
			p.Build(b)
			fmt.Println()
		}
	}
	return 0
}

// parseSearchRequests converts flags and arguments to a batched SearchBuilds
// requests.
func (r *lsRun) parseSearchRequests(args []string) (*buildbucketpb.BatchRequest, error) {
	baseReq, err := r.parseBaseRequest()
	if err != nil {
		return nil, err
	}

	ret := &buildbucketpb.BatchRequest{}
	for _, path := range args {
		searchBuilds := proto.Clone(baseReq).(*buildbucketpb.SearchBuildsRequest)
		var err error
		if searchBuilds.Predicate.Builder, err = r.parsePath(path); err != nil {
			return nil, fmt.Errorf("invalid path %q: %s", path, err)
		}
		ret.Requests = append(ret.Requests, &buildbucketpb.BatchRequest_Request{
			Request: &buildbucketpb.BatchRequest_Request_SearchBuilds{SearchBuilds: searchBuilds},
		})
	}

	// If no arguments were passed, search in any project.
	if len(ret.Requests) == 0 {
		ret.Requests = append(ret.Requests, &buildbucketpb.BatchRequest_Request{
			Request: &buildbucketpb.BatchRequest_Request_SearchBuilds{SearchBuilds: baseReq},
		})
	}

	return ret, nil
}

// parseBaseRequest returns a base SearchBuildsRequest without builder filter.
func (r *lsRun) parseBaseRequest() (*buildbucketpb.SearchBuildsRequest, error) {
	ret := &buildbucketpb.SearchBuildsRequest{
		Predicate: &buildbucketpb.BuildPredicate{},
		Fields:    r.FieldMask(),
	}

	for i, p := range ret.Fields.Paths {
		ret.Fields.Paths[i] = "builds.*." + p
	}

	// TODO(nodir): parse flags.

	return ret, nil
}

func (r *lsRun) parsePath(path string) (*buildbucketpb.BuilderID, error) {
	bid := &buildbucketpb.BuilderID{}
	switch parts := strings.Split(path, "/"); len(parts) {
	case 3:
		bid.Builder = parts[2]
		fallthrough
	case 2:
		bid.Bucket = parts[1]
		fallthrough
	case 1:
		bid.Project = parts[0]
	default:
		return nil, fmt.Errorf("got %d components, want 1-3", len(parts))
	}
	return bid, nil
}
<commit_msg>[bb] Add -cl flag to ls<commit_after>// Copyright 2019 The LUCI 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 main

import (
	"context"
	"fmt"
	"sort"
	"strings"

	"github.com/golang/protobuf/proto"
	"github.com/maruel/subcommands"
	"google.golang.org/grpc/codes"

	"go.chromium.org/luci/auth"
	"go.chromium.org/luci/common/cli"

	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
)

func cmdLS(defaultAuthOpts auth.Options) *subcommands.Command {
	return &subcommands.Command{
		UsageLine: `ls [flags] <PATH> [<PATH>...]`,
		ShortDesc: "lists builds under paths",
		LongDesc: `Lists builds under paths.

A PATH can be one of
- "<project>"
- "<project>/<bucket>"
- "<project>/<bucket>/<builder>"

Listed builds are sorted by creation time, descending.
`,
		CommandRun: func() subcommands.CommandRun {
			r := &lsRun{}
			r.RegisterGlobalFlags(defaultAuthOpts)
			r.buildFieldFlags.Register(&r.Flags)

			r.clsFlag.Register(&r.Flags, `CL URLs that builds must be associated with.
Example:
	bb ls -cl https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/1539021/1`)
			return r
		},
	}
}

type lsRun struct {
	baseCommandRun
	buildFieldFlags
	clsFlag
}

func (r *lsRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
	ctx := cli.GetContext(a, r, env)
	if err := r.initClients(ctx); err != nil {
		return r.done(ctx, err)
	}

	req, err := r.parseSearchRequests(ctx, args)
	if err != nil {
		return r.done(ctx, err)
	}

	res, err := r.client.Batch(ctx, req)
	if err != nil {
		return r.done(ctx, err)
	}

	seen := map[int64]struct{}{}
	var builds []*buildbucketpb.Build
	for _, res := range res.Responses {
		if err := res.GetError(); err != nil {
			return r.done(ctx, fmt.Errorf("%s: %s", codes.Code(err.Code), err.Message))
		}
		for _, b := range res.GetSearchBuilds().Builds {
			if _, ok := seen[b.Id]; ok {
				continue
			}
			seen[b.Id] = struct{}{}
			builds = append(builds, b)
		}
	}
	// Sort by creation time, descending.
	// Build IDs are monotonically decreasing.
	sort.Slice(builds, func(i, j int) bool { return builds[i].Id < builds[j].Id })

	p := newStdoutPrinter(r.noColor)
	for _, b := range builds {
		if r.json {
			p.JSONPB(b)
		} else {
			p.Build(b)
			fmt.Println()
		}
	}
	return 0
}

// parseSearchRequests converts flags and arguments to a batched SearchBuilds
// requests.
func (r *lsRun) parseSearchRequests(ctx context.Context, args []string) (*buildbucketpb.BatchRequest, error) {
	baseReq, err := r.parseBaseRequest(ctx)
	if err != nil {
		return nil, err
	}

	ret := &buildbucketpb.BatchRequest{}
	for _, path := range args {
		searchBuilds := proto.Clone(baseReq).(*buildbucketpb.SearchBuildsRequest)
		var err error
		if searchBuilds.Predicate.Builder, err = r.parsePath(path); err != nil {
			return nil, fmt.Errorf("invalid path %q: %s", path, err)
		}
		ret.Requests = append(ret.Requests, &buildbucketpb.BatchRequest_Request{
			Request: &buildbucketpb.BatchRequest_Request_SearchBuilds{SearchBuilds: searchBuilds},
		})
	}

	// If no arguments were passed, search in any project.
	if len(ret.Requests) == 0 {
		ret.Requests = append(ret.Requests, &buildbucketpb.BatchRequest_Request{
			Request: &buildbucketpb.BatchRequest_Request_SearchBuilds{SearchBuilds: baseReq},
		})
	}

	return ret, nil
}

// parseBaseRequest returns a base SearchBuildsRequest without builder filter.
func (r *lsRun) parseBaseRequest(ctx context.Context) (*buildbucketpb.SearchBuildsRequest, error) {
	ret := &buildbucketpb.SearchBuildsRequest{
		Predicate: &buildbucketpb.BuildPredicate{},
		Fields:    r.FieldMask(),
	}

	for i, p := range ret.Fields.Paths {
		ret.Fields.Paths[i] = "builds.*." + p
	}

	var err error
	if ret.Predicate.GerritChanges, err = r.clsFlag.retrieveCLs(ctx, r.httpClient); err != nil {
		return nil, err
	}
	return ret, nil
}

func (r *lsRun) parsePath(path string) (*buildbucketpb.BuilderID, error) {
	bid := &buildbucketpb.BuilderID{}
	switch parts := strings.Split(path, "/"); len(parts) {
	case 3:
		bid.Builder = parts[2]
		fallthrough
	case 2:
		bid.Bucket = parts[1]
		fallthrough
	case 1:
		bid.Project = parts[0]
	default:
		return nil, fmt.Errorf("got %d components, want 1-3", len(parts))
	}
	return bid, nil
}
<|endoftext|>


<commit_before>package drivers

import (
	"fmt"
	"os/exec"
	"strings"
	"syscall"

	"github.com/pkg/errors"

	"github.com/lxc/lxd/lxd/migration"
	"github.com/lxc/lxd/lxd/state"
	"github.com/lxc/lxd/shared"
	log "github.com/lxc/lxd/shared/log15"
	"github.com/lxc/lxd/shared/logger"
)

type common struct {
	name        string
	config      map[string]string
	getVolID    func(volType VolumeType, volName string) (int64, error)
	commonRules *Validators
	state       *state.State
	logger      logger.Logger
	patches     map[string]func() error
}

func (d *common) init(state *state.State, name string, config map[string]string, logger logger.Logger, volIDFunc func(volType VolumeType, volName string) (int64, error), commonRules *Validators) {
	d.name = name
	d.config = config
	d.getVolID = volIDFunc
	d.commonRules = commonRules
	d.state = state
	d.logger = logger
}

func (d *common) load() error {
	return nil
}

// validatePool validates a pool config against common rules and optional driver specific rules.
func (d *common) validatePool(config map[string]string, driverRules map[string]func(value string) error) error {
	checkedFields := map[string]struct{}{}

	// Get rules common for all drivers.
	rules := d.commonRules.PoolRules()

	// Merge driver specific rules into common rules.
	for field, validator := range driverRules {
		rules[field] = validator
	}

	// Run the validator against each field.
	for k, validator := range rules {
		checkedFields[k] = struct{}{} //Mark field as checked.
		err := validator(config[k])
		if err != nil {
			return errors.Wrapf(err, "Invalid value for pool %q option %q", d.name, k)
		}
	}

	// Look for any unchecked fields, as these are unknown fields and validation should fail.
	for k := range config {
		_, checked := checkedFields[k]
		if checked {
			continue
		}

		// User keys are not validated.
		if strings.HasPrefix(k, "user.") {
			continue
		}

		return fmt.Errorf("Invalid option for pool %q option %q", d.name, k)
	}

	return nil
}

// validateVolume validates a volume config against common rules and optional driver specific rules.
// This functions has a removeUnknownKeys option that if set to true will remove any unknown fields
// (excluding those starting with "user.") which can be used when translating a volume config to a
// different storage driver that has different options.
func (d *common) validateVolume(vol Volume, driverRules map[string]func(value string) error, removeUnknownKeys bool) error {
	checkedFields := map[string]struct{}{}

	// Get rules common for all drivers.
	rules := d.commonRules.VolumeRules(vol)

	// Merge driver specific rules into common rules.
	for field, validator := range driverRules {
		rules[field] = validator
	}

	// Run the validator against each field.
	for k, validator := range rules {
		checkedFields[k] = struct{}{} //Mark field as checked.
		err := validator(vol.config[k])
		if err != nil {
			return errors.Wrapf(err, "Invalid value for volume %q option %q", vol.name, k)
		}
	}

	// Look for any unchecked fields, as these are unknown fields and validation should fail.
	for k := range vol.config {
		_, checked := checkedFields[k]
		if checked {
			continue
		}

		// User keys are not validated.
		if strings.HasPrefix(k, "user.") {
			continue
		}

		if removeUnknownKeys {
			delete(vol.config, k)
		} else {
			return fmt.Errorf("Invalid option for volume %q option %q", vol.name, k)
		}
	}

	// If volume type is not custom, don't allow "size" property.
	if vol.volType != VolumeTypeCustom && vol.config["size"] != "" {
		return fmt.Errorf("Volume %q property is only valid for custom volume types", "size")
	}

	return nil
}

// MigrationType returns the type of transfer methods to be used when doing migrations between pools
// in preference order.
func (d *common) MigrationTypes(contentType ContentType, refresh bool) []migration.Type {
	return []migration.Type{
		{
			FSType:   migration.MigrationFSType_RSYNC,
			Features: []string{"xattrs", "delete", "compress", "bidirectional"},
		},
	}
}

// Name returns the pool name.
func (d *common) Name() string {
	return d.name
}

// Logger returns the current logger.
func (d *common) Logger() logger.Logger {
	return d.logger
}

// Config returns the storage pool config (as a copy, so not modifiable).
func (d *common) Config() map[string]string {
	confCopy := make(map[string]string, len(d.config))
	for k, v := range d.config {
		confCopy[k] = v
	}

	return confCopy
}

// ApplyPatch looks for a suitable patch and runs it.
func (d *common) ApplyPatch(name string) error {
	if d.patches == nil {
		return fmt.Errorf("The patch mechanism isn't implemented on pool '%s'", d.name)
	}

	// Locate the patch.
	patch, ok := d.patches[name]
	if !ok {
		return fmt.Errorf("Patch '%s' isn't implemented on pool '%s'", name, d.name)
	}

	// Handle cases where a patch isn't needed.
	if patch == nil {
		return nil
	}

	return patch()
}

// moveGPTAltHeader moves the GPT alternative header to the end of the disk device supplied.
// If the device supplied is not detected as not being a GPT disk then no action is taken and nil is returned.
// If the required sgdisk command is not available a warning is logged, but no error is returned, as really it is
// the job of the VM quest to ensure the partitions are resized to the size of the disk (as LXD does not dicatate
// what partition structure (if any) the disk should have. However we do attempt to move the GPT alternative
// header where possible so that the backup header is where it is expected in case of any corruption with the
// primary header.
func (d *common) moveGPTAltHeader(devPath string) error {
	path, err := exec.LookPath("sgdisk")
	if err != nil {
		d.logger.Warn("Skipped moving GPT alternative header to end of disk as sgdisk command not found", log.Ctx{"dev": devPath})
		return nil
	}

	_, err = shared.RunCommand(path, "--move-second-header", devPath)
	if err == nil {
		d.logger.Debug("Moved GPT alternative header to end of disk", log.Ctx{"dev": devPath})
		return nil
	}

	runErr, ok := err.(shared.RunError)
	if ok {
		exitError, ok := runErr.Err.(*exec.ExitError)
		if ok {
			waitStatus := exitError.Sys().(syscall.WaitStatus)

			// sgdisk manpage says exit status 3 means:
			// "Non-GPT disk detected and no -g option, but operation requires a write action".
			if waitStatus.ExitStatus() == 3 {
				return nil // Non-error as non-GPT disk specified.
			}
		}
	}

	return err
}
<commit_msg>lxd/storage/drivers/driver/common: Updates MigrationTypes to support block volumes for VMs<commit_after>package drivers

import (
	"fmt"
	"os/exec"
	"strings"
	"syscall"

	"github.com/pkg/errors"

	"github.com/lxc/lxd/lxd/migration"
	"github.com/lxc/lxd/lxd/state"
	"github.com/lxc/lxd/shared"
	log "github.com/lxc/lxd/shared/log15"
	"github.com/lxc/lxd/shared/logger"
)

type common struct {
	name        string
	config      map[string]string
	getVolID    func(volType VolumeType, volName string) (int64, error)
	commonRules *Validators
	state       *state.State
	logger      logger.Logger
	patches     map[string]func() error
}

func (d *common) init(state *state.State, name string, config map[string]string, logger logger.Logger, volIDFunc func(volType VolumeType, volName string) (int64, error), commonRules *Validators) {
	d.name = name
	d.config = config
	d.getVolID = volIDFunc
	d.commonRules = commonRules
	d.state = state
	d.logger = logger
}

func (d *common) load() error {
	return nil
}

// validatePool validates a pool config against common rules and optional driver specific rules.
func (d *common) validatePool(config map[string]string, driverRules map[string]func(value string) error) error {
	checkedFields := map[string]struct{}{}

	// Get rules common for all drivers.
	rules := d.commonRules.PoolRules()

	// Merge driver specific rules into common rules.
	for field, validator := range driverRules {
		rules[field] = validator
	}

	// Run the validator against each field.
	for k, validator := range rules {
		checkedFields[k] = struct{}{} //Mark field as checked.
		err := validator(config[k])
		if err != nil {
			return errors.Wrapf(err, "Invalid value for pool %q option %q", d.name, k)
		}
	}

	// Look for any unchecked fields, as these are unknown fields and validation should fail.
	for k := range config {
		_, checked := checkedFields[k]
		if checked {
			continue
		}

		// User keys are not validated.
		if strings.HasPrefix(k, "user.") {
			continue
		}

		return fmt.Errorf("Invalid option for pool %q option %q", d.name, k)
	}

	return nil
}

// validateVolume validates a volume config against common rules and optional driver specific rules.
// This functions has a removeUnknownKeys option that if set to true will remove any unknown fields
// (excluding those starting with "user.") which can be used when translating a volume config to a
// different storage driver that has different options.
func (d *common) validateVolume(vol Volume, driverRules map[string]func(value string) error, removeUnknownKeys bool) error {
	checkedFields := map[string]struct{}{}

	// Get rules common for all drivers.
	rules := d.commonRules.VolumeRules(vol)

	// Merge driver specific rules into common rules.
	for field, validator := range driverRules {
		rules[field] = validator
	}

	// Run the validator against each field.
	for k, validator := range rules {
		checkedFields[k] = struct{}{} //Mark field as checked.
		err := validator(vol.config[k])
		if err != nil {
			return errors.Wrapf(err, "Invalid value for volume %q option %q", vol.name, k)
		}
	}

	// Look for any unchecked fields, as these are unknown fields and validation should fail.
	for k := range vol.config {
		_, checked := checkedFields[k]
		if checked {
			continue
		}

		// User keys are not validated.
		if strings.HasPrefix(k, "user.") {
			continue
		}

		if removeUnknownKeys {
			delete(vol.config, k)
		} else {
			return fmt.Errorf("Invalid option for volume %q option %q", vol.name, k)
		}
	}

	// If volume type is not custom, don't allow "size" property.
	if vol.volType != VolumeTypeCustom && vol.config["size"] != "" {
		return fmt.Errorf("Volume %q property is only valid for custom volume types", "size")
	}

	return nil
}

// MigrationType returns the type of transfer methods to be used when doing migrations between pools
// in preference order.
func (d *common) MigrationTypes(contentType ContentType, refresh bool) []migration.Type {
	var transportType migration.MigrationFSType

	if contentType == ContentTypeBlock {
		transportType = migration.MigrationFSType_BLOCK_AND_RSYNC
	} else {
		transportType = migration.MigrationFSType_RSYNC
	}

	return []migration.Type{
		{
			FSType:   transportType,
			Features: []string{"xattrs", "delete", "compress", "bidirectional"},
		},
	}
}

// Name returns the pool name.
func (d *common) Name() string {
	return d.name
}

// Logger returns the current logger.
func (d *common) Logger() logger.Logger {
	return d.logger
}

// Config returns the storage pool config (as a copy, so not modifiable).
func (d *common) Config() map[string]string {
	confCopy := make(map[string]string, len(d.config))
	for k, v := range d.config {
		confCopy[k] = v
	}

	return confCopy
}

// ApplyPatch looks for a suitable patch and runs it.
func (d *common) ApplyPatch(name string) error {
	if d.patches == nil {
		return fmt.Errorf("The patch mechanism isn't implemented on pool '%s'", d.name)
	}

	// Locate the patch.
	patch, ok := d.patches[name]
	if !ok {
		return fmt.Errorf("Patch '%s' isn't implemented on pool '%s'", name, d.name)
	}

	// Handle cases where a patch isn't needed.
	if patch == nil {
		return nil
	}

	return patch()
}

// moveGPTAltHeader moves the GPT alternative header to the end of the disk device supplied.
// If the device supplied is not detected as not being a GPT disk then no action is taken and nil is returned.
// If the required sgdisk command is not available a warning is logged, but no error is returned, as really it is
// the job of the VM quest to ensure the partitions are resized to the size of the disk (as LXD does not dicatate
// what partition structure (if any) the disk should have. However we do attempt to move the GPT alternative
// header where possible so that the backup header is where it is expected in case of any corruption with the
// primary header.
func (d *common) moveGPTAltHeader(devPath string) error {
	path, err := exec.LookPath("sgdisk")
	if err != nil {
		d.logger.Warn("Skipped moving GPT alternative header to end of disk as sgdisk command not found", log.Ctx{"dev": devPath})
		return nil
	}

	_, err = shared.RunCommand(path, "--move-second-header", devPath)
	if err == nil {
		d.logger.Debug("Moved GPT alternative header to end of disk", log.Ctx{"dev": devPath})
		return nil
	}

	runErr, ok := err.(shared.RunError)
	if ok {
		exitError, ok := runErr.Err.(*exec.ExitError)
		if ok {
			waitStatus := exitError.Sys().(syscall.WaitStatus)

			// sgdisk manpage says exit status 3 means:
			// "Non-GPT disk detected and no -g option, but operation requires a write action".
			if waitStatus.ExitStatus() == 3 {
				return nil // Non-error as non-GPT disk specified.
			}
		}
	}

	return err
}
<|endoftext|>


<commit_before>package s3

import (
	"fmt"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"github.com/gobwas/glob"
	"github.com/gogo/protobuf/types"
	"github.com/pachyderm/pachyderm/src/client"
	"github.com/pachyderm/pachyderm/src/client/pfs"
	"github.com/pachyderm/pachyderm/src/server/pkg/errutil"
)

const defaultMaxKeys int = 1000

// the raw XML returned for a request to get the location of a bucket
const locationSource = `<?xml version="1.0" encoding="UTF-8"?>
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">PACHYDERM</LocationConstraint>`

// ListBucketResult is an XML-encodable listing of files/objects in a
// repo/bucket
type ListBucketResult struct {
	Name           string           `xml:"Name"`
	Prefix         string           `xml:"Prefix"`
	Marker         string           `xml:"Marker"`
	MaxKeys        int              `xml:"MaxKeys"`
	IsTruncated    bool             `xml:"IsTruncated"`
	Contents       []Contents       `xml:"Contents"`
	CommonPrefixes []CommonPrefixes `xml:"CommonPrefixes"`
}

func (r *ListBucketResult) isFull() bool {
	return len(r.Contents)+len(r.CommonPrefixes) >= r.MaxKeys
}

// Contents is an individual file/object
type Contents struct {
	Key          string    `xml:"Key"`
	LastModified time.Time `xml:"LastModified"`
	ETag         string    `xml:"ETag"`
	Size         uint64    `xml:"Size"`
	StorageClass string    `xml:"StorageClass"`
	Owner        User      `xml:"Owner"`
}

func newContents(fileInfo *pfs.FileInfo) (Contents, error) {
	t, err := types.TimestampFromProto(fileInfo.Committed)
	if err != nil {
		return Contents{}, err
	}

	return Contents{
		Key:          fileInfo.File.Path,
		LastModified: t,
		ETag:         "",
		Size:         fileInfo.SizeBytes,
		StorageClass: storageClass,
		Owner:        defaultUser,
	}, nil
}

// CommonPrefixes is an individual PFS directory
type CommonPrefixes struct {
	Prefix string `xml:"Prefix"`
}

func newCommonPrefixes(dir string) CommonPrefixes {
	return CommonPrefixes{
		Prefix: fmt.Sprintf("%s/", dir),
	}
}

type bucketHandler struct {
	pc *client.APIClient
}

func newBucketHandler(pc *client.APIClient) bucketHandler {
	return bucketHandler{pc: pc}
}

func (h bucketHandler) location(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)
	_, err := h.pc.InspectBranch(repo, branch)
	if err != nil {
		newNotFoundError(r, err).write(w)
		return
	}

	w.Header().Set("Content-Type", "application/xml")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(locationSource))
}

func (h bucketHandler) get(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)

	// ensure the branch exists and has a head
	branchInfo, err := h.pc.InspectBranch(repo, branch)
	if err != nil {
		newNotFoundError(r, err).write(w)
		return
	}

	result := &ListBucketResult{
		Name:        repo,
		Prefix:      r.FormValue("prefix"),
		Marker:      r.FormValue("marker"),
		MaxKeys:     intFormValue(r, "max-keys", 1, defaultMaxKeys, defaultMaxKeys),
		IsTruncated: false,
	}

	delimiter := r.FormValue("delimiter")
	if delimiter != "" && delimiter != "/" {
		newInvalidDelimiterError(r).write(w)
		return
	}

	if branchInfo.Head == nil {
		// if there's no head commit, just print an empty list of files
		writeXML(w, http.StatusOK, result)
	} else if delimiter == "" {
		h.listRecursive(w, r, result, branch)
	} else {
		h.list(w, r, result, branch)
	}
}

func (h bucketHandler) listRecursive(w http.ResponseWriter, r *http.Request, result *ListBucketResult, branch string) {
	err := h.pc.Walk(result.Name, branch, filepath.Dir(result.Prefix), func(fileInfo *pfs.FileInfo) error {
		if !shouldShowFileInfo(branch, result.Marker, fileInfo) {
			return nil
		}
		fileInfo.File.Path = fileInfo.File.Path[1:] // strip leading slash
		if !strings.HasPrefix(fileInfo.File.Path, result.Prefix) {
			return nil
		}
		if result.isFull() {
			result.IsTruncated = true
			return errutil.ErrBreak
		}
		if fileInfo.FileType == pfs.FileType_FILE {
			contents, err := newContents(fileInfo)
			if err != nil {
				return err
			}
			result.Contents = append(result.Contents, contents)
		} else {
			result.CommonPrefixes = append(result.CommonPrefixes, newCommonPrefixes(fileInfo.File.Path))
		}
		return nil
	})

	if err != nil {
		newInternalError(r, err).write(w)
		return
	}

	writeXML(w, http.StatusOK, result)
}

func (h bucketHandler) list(w http.ResponseWriter, r *http.Request, result *ListBucketResult, branch string) {
	pattern := fmt.Sprintf("%s*", glob.QuoteMeta(result.Prefix))
	fileInfos, err := h.pc.GlobFile(result.Name, branch, pattern)
	if err != nil {
		newInternalError(r, err).write(w)
		return
	}

	for _, fileInfo := range fileInfos {
		if !shouldShowFileInfo(branch, result.Marker, fileInfo) {
			continue
		}
		fileInfo.File.Path = fileInfo.File.Path[1:] // strip leading slash
		if result.isFull() {
			result.IsTruncated = true
			break
		}
		if fileInfo.FileType == pfs.FileType_FILE {
			contents, err := newContents(fileInfo)
			if err != nil {
				newInternalError(r, err).write(w)
				return
			}
			result.Contents = append(result.Contents, contents)
		} else {
			result.CommonPrefixes = append(result.CommonPrefixes, newCommonPrefixes(fileInfo.File.Path))
		}
	}

	writeXML(w, http.StatusOK, result)
}

func (h bucketHandler) put(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)

	err := h.pc.CreateRepo(repo)
	if err != nil {
		if strings.Contains(err.Error(), "as it already exists") {
			// Bucket already exists - this is not an error so long as the
			// branch being created is new. Verify if that is the case now,
			// since PFS' `CreateBranch` won't error out.
			_, err := h.pc.InspectBranch(repo, branch)
			if err != nil {
				if !branchNotFoundMatcher.MatchString(err.Error()) {
					newInternalError(r, err).write(w)
					return
				}
			} else {
				newBucketAlreadyExistsError(r).write(w)
				return
			}
		} else {
			newInternalError(r, err).write(w)
			return
		}
	}

	err = h.pc.CreateBranch(repo, branch, "", nil)
	if err != nil {
		newInternalError(r, err).write(w)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func (h bucketHandler) del(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)

	err := h.pc.DeleteBranch(repo, branch, false)
	if err != nil {
		newNotFoundError(r, err).write(w)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func shouldShowFileInfo(branch, marker string, fileInfo *pfs.FileInfo) bool {
	if fileInfo.FileType != pfs.FileType_FILE && fileInfo.FileType != pfs.FileType_DIR {
		// skip anything that isn't a file or dir
		return false
	}
	if fileInfo.FileType == pfs.FileType_DIR && fileInfo.File.Path == "/" {
		// skip the root directory
		return false
	}
	if fileInfo.File.Path <= marker {
		// skip file paths below the marker
		return false
	}

	return true
}
<commit_msg>Fixed marker bug<commit_after>package s3

import (
	"fmt"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"github.com/gobwas/glob"
	"github.com/gogo/protobuf/types"
	"github.com/pachyderm/pachyderm/src/client"
	"github.com/pachyderm/pachyderm/src/client/pfs"
	"github.com/pachyderm/pachyderm/src/server/pkg/errutil"
)

const defaultMaxKeys int = 1000

// the raw XML returned for a request to get the location of a bucket
const locationSource = `<?xml version="1.0" encoding="UTF-8"?>
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">PACHYDERM</LocationConstraint>`

// ListBucketResult is an XML-encodable listing of files/objects in a
// repo/bucket
type ListBucketResult struct {
	Name           string           `xml:"Name"`
	Prefix         string           `xml:"Prefix"`
	Marker         string           `xml:"Marker"`
	MaxKeys        int              `xml:"MaxKeys"`
	IsTruncated    bool             `xml:"IsTruncated"`
	Contents       []Contents       `xml:"Contents"`
	CommonPrefixes []CommonPrefixes `xml:"CommonPrefixes"`
}

func (r *ListBucketResult) isFull() bool {
	return len(r.Contents)+len(r.CommonPrefixes) >= r.MaxKeys
}

// Contents is an individual file/object
type Contents struct {
	Key          string    `xml:"Key"`
	LastModified time.Time `xml:"LastModified"`
	ETag         string    `xml:"ETag"`
	Size         uint64    `xml:"Size"`
	StorageClass string    `xml:"StorageClass"`
	Owner        User      `xml:"Owner"`
}

func newContents(fileInfo *pfs.FileInfo) (Contents, error) {
	t, err := types.TimestampFromProto(fileInfo.Committed)
	if err != nil {
		return Contents{}, err
	}

	return Contents{
		Key:          fileInfo.File.Path,
		LastModified: t,
		ETag:         "",
		Size:         fileInfo.SizeBytes,
		StorageClass: storageClass,
		Owner:        defaultUser,
	}, nil
}

// CommonPrefixes is an individual PFS directory
type CommonPrefixes struct {
	Prefix string `xml:"Prefix"`
}

func newCommonPrefixes(dir string) CommonPrefixes {
	return CommonPrefixes{
		Prefix: fmt.Sprintf("%s/", dir),
	}
}

type bucketHandler struct {
	pc *client.APIClient
}

func newBucketHandler(pc *client.APIClient) bucketHandler {
	return bucketHandler{pc: pc}
}

func (h bucketHandler) location(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)
	_, err := h.pc.InspectBranch(repo, branch)
	if err != nil {
		newNotFoundError(r, err).write(w)
		return
	}

	w.Header().Set("Content-Type", "application/xml")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(locationSource))
}

func (h bucketHandler) get(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)

	// ensure the branch exists and has a head
	branchInfo, err := h.pc.InspectBranch(repo, branch)
	if err != nil {
		newNotFoundError(r, err).write(w)
		return
	}

	result := &ListBucketResult{
		Name:        repo,
		Prefix:      r.FormValue("prefix"),
		Marker:      r.FormValue("marker"),
		MaxKeys:     intFormValue(r, "max-keys", 1, defaultMaxKeys, defaultMaxKeys),
		IsTruncated: false,
	}

	delimiter := r.FormValue("delimiter")
	if delimiter != "" && delimiter != "/" {
		newInvalidDelimiterError(r).write(w)
		return
	}

	if branchInfo.Head == nil {
		// if there's no head commit, just print an empty list of files
		writeXML(w, http.StatusOK, result)
	} else if delimiter == "" {
		h.listRecursive(w, r, result, branch)
	} else {
		h.list(w, r, result, branch)
	}
}

func (h bucketHandler) listRecursive(w http.ResponseWriter, r *http.Request, result *ListBucketResult, branch string) {
	err := h.pc.Walk(result.Name, branch, filepath.Dir(result.Prefix), func(fileInfo *pfs.FileInfo) error {
		fileInfo = updateFileInfo(branch, result.Marker, fileInfo)
		if fileInfo == nil {
			return nil
		}
		if !strings.HasPrefix(fileInfo.File.Path, result.Prefix) {
			return nil
		}
		if result.isFull() {
			result.IsTruncated = true
			return errutil.ErrBreak
		}
		if fileInfo.FileType == pfs.FileType_FILE {
			contents, err := newContents(fileInfo)
			if err != nil {
				return err
			}
			result.Contents = append(result.Contents, contents)
		} else {
			result.CommonPrefixes = append(result.CommonPrefixes, newCommonPrefixes(fileInfo.File.Path))
		}
		return nil
	})

	if err != nil {
		newInternalError(r, err).write(w)
		return
	}

	writeXML(w, http.StatusOK, result)
}

func (h bucketHandler) list(w http.ResponseWriter, r *http.Request, result *ListBucketResult, branch string) {
	pattern := fmt.Sprintf("%s*", glob.QuoteMeta(result.Prefix))
	fileInfos, err := h.pc.GlobFile(result.Name, branch, pattern)
	if err != nil {
		newInternalError(r, err).write(w)
		return
	}

	for _, fileInfo := range fileInfos {
		fileInfo = updateFileInfo(branch, result.Marker, fileInfo)
		if fileInfo == nil {
			break
		}
		if result.isFull() {
			result.IsTruncated = true
			break
		}
		if fileInfo.FileType == pfs.FileType_FILE {
			contents, err := newContents(fileInfo)
			if err != nil {
				newInternalError(r, err).write(w)
				return
			}
			result.Contents = append(result.Contents, contents)
		} else {
			result.CommonPrefixes = append(result.CommonPrefixes, newCommonPrefixes(fileInfo.File.Path))
		}
	}

	writeXML(w, http.StatusOK, result)
}

func (h bucketHandler) put(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)

	err := h.pc.CreateRepo(repo)
	if err != nil {
		if strings.Contains(err.Error(), "as it already exists") {
			// Bucket already exists - this is not an error so long as the
			// branch being created is new. Verify if that is the case now,
			// since PFS' `CreateBranch` won't error out.
			_, err := h.pc.InspectBranch(repo, branch)
			if err != nil {
				if !branchNotFoundMatcher.MatchString(err.Error()) {
					newInternalError(r, err).write(w)
					return
				}
			} else {
				newBucketAlreadyExistsError(r).write(w)
				return
			}
		} else {
			newInternalError(r, err).write(w)
			return
		}
	}

	err = h.pc.CreateBranch(repo, branch, "", nil)
	if err != nil {
		newInternalError(r, err).write(w)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func (h bucketHandler) del(w http.ResponseWriter, r *http.Request) {
	repo, branch := bucketArgs(r)

	err := h.pc.DeleteBranch(repo, branch, false)
	if err != nil {
		newNotFoundError(r, err).write(w)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// updateFileInfo takes in a `FileInfo`, and updates it to be used in s3
// object listings:
// 1) if nil is returned, the `FileInfo` should not be included in the list
// 2) the path is updated to remove the leading slash
func updateFileInfo(branch, marker string, fileInfo *pfs.FileInfo) *pfs.FileInfo {
	if fileInfo.FileType != pfs.FileType_FILE && fileInfo.FileType != pfs.FileType_DIR {
		// skip anything that isn't a file or dir
		return nil
	}
	if fileInfo.FileType == pfs.FileType_DIR && fileInfo.File.Path == "/" {
		// skip the root directory
		return nil
	}
	fileInfo.File.Path = fileInfo.File.Path[1:] // strip leading slash
	if fileInfo.File.Path <= marker {
		// skip file paths below the marker
		return nil
	}

	return fileInfo
}
<|endoftext|>


<commit_before><commit_msg>subresource_api_test: use libvmi.NewCirros to define vmis<commit_after><|endoftext|>


<commit_before>package nano

import (
	"fmt"
	"log"
	"sync/atomic"
	"time"
)

const (
	timerBacklog = 512
	loopForever  = -1
)

var (
	// timerManager manager for all timers
	timerManager = &struct {
		incrementId    int64            // auto increment id
		timers         map[int64]*Timer // all timers
		chClosingTimer chan int64       // timer for closing
		chCreatedTimer chan *Timer
	}{}

	// timerPrecision indicates the precision of timer, default is time.Second
	timerPrecision = time.Second

	// globalTicker represents global ticker that all cron job will be executed
	// in globalTicker.
	globalTicker *time.Ticker
)

type (
	// TimerFunc represents a function which will be called periodically in main
	// logic gorontine.
	TimerFunc func()

	// Timer represents a cron job
	Timer struct {
		id       int64         // timer id
		fn       TimerFunc     // function that execute
		createAt int64         // timer create time
		interval time.Duration // execution interval
		elapse   int64         // total elapse time
		closed   int32         // is timer closed
		counter  int           // counter
	}
)

func init() {
	timerManager.timers = map[int64]*Timer{}
	timerManager.chClosingTimer = make(chan int64, timerBacklog)
	timerManager.chCreatedTimer = make(chan *Timer, timerBacklog)
}

// ID returns id of current timer
func (t *Timer) ID() int64 {
	return t.id
}

// Stop turns off a timer. After Stop, fn will not be called forever
func (t *Timer) Stop() {
	if atomic.LoadInt32(&t.closed) > 0 {
		return
	}
	atomic.StoreInt32(&t.closed, 1)
	timerManager.chClosingTimer <- t.id
}

// call job function with protection
func pjob(id int64, fn TimerFunc) {
	defer func() {
		if err := recover(); err != nil {
			log.Println(fmt.Sprintf("Call timer funtion error, TimerID=%d, Error=%v\n%s", id, err, stack()))
		}
	}()

	fn()
}

// TODO: if closing timers'count in single cron call more than timerBacklog will case problem.
func cron() {
	now := time.Now().UnixNano()
	for id, t := range timerManager.timers {
		if t.createAt+t.elapse <= now {
			pjob(id, t.fn)
			t.elapse += int64(t.interval)

			// check timer counter
			if t.counter != loopForever && t.counter > 0 {
				t.counter--
				if t.counter == 0 {
					t.Stop()
				}
			}
		}
	}
}

// NewTimer returns a new Timer containing a function that will be called
// with a period specified by the duration argument. It adjusts the intervals
// for slow receivers.
// The duration d must be greater than zero; if not, NewTimer will panic.
// Stop the timer to release associated resources.
func NewTimer(interval time.Duration, fn TimerFunc) *Timer {
	return NewCountTimer(interval, loopForever, fn)
}

// NewCountTimer returns a new Timer containing a function that will be called
// with a period specified by the duration argument. After count times, timer
// will be stopped automatically, It adjusts the intervals for slow receivers.
// The duration d must be greater than zero; if not, NewTimer will panic.
// Stop the timer to release associated resources.
func NewCountTimer(interval time.Duration, count int, fn TimerFunc) *Timer {
	if fn == nil {
		panic("nano/timer: nil timer function")
	}
	if interval <= 0 {
		panic("non-positive interval for NewTimer")
	}

	id := atomic.AddInt64(&timerManager.incrementId, 1)
	t := &Timer{
		id:       id,
		fn:       fn,
		createAt: time.Now().UnixNano(),
		interval: interval,
		elapse:   int64(interval), // first execution will be after interval
		counter:  count,
	}

	// add to manager
	timerManager.chCreatedTimer <- t
	return t
}

// SetTimerPrecision set the ticker precision, and time precision can not less
// than a Millisecond, and can not change after application running.
func SetTimerPrecision(precision time.Duration) {
	if precision < time.Millisecond {
		panic("time precision can not less than a Millisecond")
	}
	timerPrecision = precision
}
<commit_msg>feature: allow customize timer created/closing backlog<commit_after>package nano

import (
	"fmt"
	"log"
	"sync/atomic"
	"time"
)

const (
	loopForever = -1
)

var (
	// default timer backlog
	timerBacklog = 128

	// timerManager manager for all timers
	timerManager = &struct {
		incrementId    int64            // auto increment id
		timers         map[int64]*Timer // all timers
		chClosingTimer chan int64       // timer for closing
		chCreatedTimer chan *Timer
	}{}

	// timerPrecision indicates the precision of timer, default is time.Second
	timerPrecision = time.Second

	// globalTicker represents global ticker that all cron job will be executed
	// in globalTicker.
	globalTicker *time.Ticker
)

type (
	// TimerFunc represents a function which will be called periodically in main
	// logic gorontine.
	TimerFunc func()

	// Timer represents a cron job
	Timer struct {
		id       int64         // timer id
		fn       TimerFunc     // function that execute
		createAt int64         // timer create time
		interval time.Duration // execution interval
		elapse   int64         // total elapse time
		closed   int32         // is timer closed
		counter  int           // counter
	}
)

func init() {
	timerManager.timers = map[int64]*Timer{}
	timerManager.chClosingTimer = make(chan int64, timerBacklog)
	timerManager.chCreatedTimer = make(chan *Timer, timerBacklog)
}

// ID returns id of current timer
func (t *Timer) ID() int64 {
	return t.id
}

// Stop turns off a timer. After Stop, fn will not be called forever
func (t *Timer) Stop() {
	if atomic.LoadInt32(&t.closed) > 0 {
		return
	}
	atomic.StoreInt32(&t.closed, 1)
	timerManager.chClosingTimer <- t.id
}

// call job function with protection
func pjob(id int64, fn TimerFunc) {
	defer func() {
		if err := recover(); err != nil {
			log.Println(fmt.Sprintf("Call timer funtion error, TimerID=%d, Error=%v\n%s", id, err, stack()))
		}
	}()

	fn()
}

// TODO: if closing timers'count in single cron call more than timerBacklog will case problem.
func cron() {
	now := time.Now().UnixNano()
	for id, t := range timerManager.timers {
		// prevent chClosingTimer exceed
		if t.counter == 0 && len(timerManager.chClosingTimer) < timerBacklog {
			t.Stop()
			continue
		}

		// execute job
		if t.createAt+t.elapse <= now {
			pjob(id, t.fn)
			t.elapse += int64(t.interval)

			// update timer counter
			if t.counter != loopForever && t.counter > 0 {
				t.counter--
			}
		}
	}
}

// NewTimer returns a new Timer containing a function that will be called
// with a period specified by the duration argument. It adjusts the intervals
// for slow receivers.
// The duration d must be greater than zero; if not, NewTimer will panic.
// Stop the timer to release associated resources.
func NewTimer(interval time.Duration, fn TimerFunc) *Timer {
	return NewCountTimer(interval, loopForever, fn)
}

// NewCountTimer returns a new Timer containing a function that will be called
// with a period specified by the duration argument. After count times, timer
// will be stopped automatically, It adjusts the intervals for slow receivers.
// The duration d must be greater than zero; if not, NewTimer will panic.
// Stop the timer to release associated resources.
func NewCountTimer(interval time.Duration, count int, fn TimerFunc) *Timer {
	if fn == nil {
		panic("nano/timer: nil timer function")
	}
	if interval <= 0 {
		panic("non-positive interval for NewTimer")
	}

	id := atomic.AddInt64(&timerManager.incrementId, 1)
	t := &Timer{
		id:       id,
		fn:       fn,
		createAt: time.Now().UnixNano(),
		interval: interval,
		elapse:   int64(interval), // first execution will be after interval
		counter:  count,
	}

	// add to manager
	timerManager.chCreatedTimer <- t
	return t
}

// SetTimerPrecision set the ticker precision, and time precision can not less
// than a Millisecond, and can not change after application running. The default
// precision is time.Second
func SetTimerPrecision(precision time.Duration) {
	if precision < time.Millisecond {
		panic("time precision can not less than a Millisecond")
	}
	timerPrecision = precision
}

// SetTimerBacklog set the timer created/closing channel backlog, A small backlog
// may cause the logic to be blocked when call NewTimer/NewCountTimer/timer.Stop
// in main logic gorontine.
func SetTimerBacklog(c int) {
	if c < 16 {
		c = 16
	}
	timerBacklog = c
}
<|endoftext|>


<commit_before>package main_test

import (
	"encoding/json"
	"os/exec"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"

	. "github.com/concourse/time-resource/check"
	"github.com/concourse/time-resource/models"
)

var _ = Describe("Check", func() {
	var (
		checkCmd *exec.Cmd
		now      time.Time
	)

	BeforeEach(func() {
		now = time.Now().UTC()
	})

	Describe("ParseTime", func() {
		It("can parse many formats", func() {
			expectedTime := time.Date(0, 1, 1, 21, 0, 0, 0, time.UTC)

			formats := []string{
				"1:00 PM -0800",
				"1PM -0800",
				"1 PM -0800",
				"13:00 -0800",
				"1300 -0800",
			}

			for _, format := range formats {
				By("working with " + format)
				parsedTime, err := ParseTime(format)

				Expect(err).NotTo(HaveOccurred())
				Expect(parsedTime.Equal(expectedTime)).To(BeTrue())
			}
		})
	})

	Describe("ParseWeekday", func() {
		It("can parse a weekday", func() {
			parsedWeekdays, err := ParseWeekdays([]string{"Monday", "Tuesday"})

			Expect(err).NotTo(HaveOccurred())
			Expect(parsedWeekdays).To(Equal([]time.Weekday{time.Monday, time.Tuesday}))
		})

		It("raise error if weekday can't be parsed", func() {
			_, err := ParseWeekdays([]string{"Foo", "Tuesday"})

			Expect(err).To(HaveOccurred())
		})
	})

	Describe("IsInDays", func() {
		It("returns true if current day is in dayslist", func() {
			daysList := []time.Weekday{
				now.Weekday(),
				now.Add(24 * time.Hour).Weekday(),
			}

			Expect(IsInDays(now, daysList)).To(BeTrue())
		})

		It("return true if list is empty", func() {
			Expect(IsInDays(now, nil)).To(BeTrue())
		})

		It("returns false if not in list", func() {
			daysList := []time.Weekday{
				now.Add(24 * time.Hour).Weekday(),
				now.Add(48 * time.Hour).Weekday(),
			}

			Expect(IsInDays(now, daysList)).To(BeFalse())
		})
	})

	BeforeEach(func() {
		checkCmd = exec.Command(checkPath)
	})

	Context("with invalid inputs", func() {
		var request models.CheckRequest
		var response models.CheckResponse
		var session *gexec.Session

		BeforeEach(func() {
			request = models.CheckRequest{}
			response = models.CheckResponse{}
		})

		JustBeforeEach(func() {
			var err error

			stdin, err := checkCmd.StdinPipe()
			Expect(err).NotTo(HaveOccurred())

			session, err = gexec.Start(checkCmd, GinkgoWriter, GinkgoWriter)
			Expect(err).NotTo(HaveOccurred())

			err = json.NewEncoder(stdin).Encode(request)
			Expect(err).NotTo(HaveOccurred())
		})

		Context("with a missing everything", func() {
			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("one of 'interval' or 'between' must be specified"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid start", func() {
			BeforeEach(func() {
				request.Source.Start = "not-a-time"
				request.Source.Stop = "3:04 PM -0700"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid start time"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid stop", func() {
			BeforeEach(func() {
				request.Source.Start = "3:04 PM -0700"
				request.Source.Stop = "not-a-time"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid stop time"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with a missing stop", func() {
			BeforeEach(func() {
				request.Source.Start = "3:04 PM -0700"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("empty stop time!"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with a missing start", func() {
			BeforeEach(func() {
				request.Source.Stop = "3:04 PM -0700"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("empty start time!"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid interval ", func() {
			BeforeEach(func() {
				request.Source.Interval = "not-an-interval"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid interval"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid day ", func() {
			BeforeEach(func() {
				request.Source.Days = []string{"Foo", "Bar"}
				request.Source.Interval = "1m"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid day 'Foo'"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})
	})

	Context("when executed", func() {
		var request models.CheckRequest
		var response models.CheckResponse

		BeforeEach(func() {
			request = models.CheckRequest{}
			response = models.CheckResponse{}
		})

		JustBeforeEach(func() {
			stdin, err := checkCmd.StdinPipe()
			Expect(err).NotTo(HaveOccurred())

			session, err := gexec.Start(checkCmd, GinkgoWriter, GinkgoWriter)
			Expect(err).NotTo(HaveOccurred())

			err = json.NewEncoder(stdin).Encode(request)
			Expect(err).NotTo(HaveOccurred())

			Eventually(session).Should(gexec.Exit(0))

			err = json.Unmarshal(session.Out.Contents(), &response)
			Expect(err).NotTo(HaveOccurred())
		})

		Context("when a time range is specified", func() {
			Context("when we are in the specified time range", func() {
				BeforeEach(func() {
					start := now.Add(-1 * time.Hour)
					stop := now.Add(1 * time.Hour)
					timeLayout := "3:04 PM -0700"

					request.Source.Start = start.Format(timeLayout)
					request.Source.Stop = stop.Format(timeLayout)
				})

				Context("when no version is given", func() {
					It("outputs a version containing the current time", func() {
						Expect(response).To(HaveLen(1))
						Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})

				Context("when a version is given", func() {
					Context("when the resource has already triggered with in the current time range", func() {
						BeforeEach(func() {
							request.Version.Time = now.Add(-30 * time.Minute)
						})

						It("outputs a supplied version", func() {
							Expect(response).To(HaveLen(1))
							Expect(response[0].Time).To(Equal(request.Version.Time))
						})
					})

					Context("when the resource was triggered yesterday near the end of the time frame", func() {
						BeforeEach(func() {
							request.Version.Time = now.Add(-23 * time.Hour)
						})

						It("outputs a version containing the current time and supplied version", func() {
							Expect(response).To(HaveLen(2))
							Expect(response[0].Time).To(Equal(request.Version.Time))
							Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
						})
					})

					Context("when the resource was triggered yesterday in the current time frame", func() {
						BeforeEach(func() {
							request.Version.Time = now.Add(-24 * time.Hour)
						})

						It("outputs a version containing the current time and supplied version", func() {
							Expect(response).To(HaveLen(2))
							Expect(response[0].Time).To(Equal(request.Version.Time))
							Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
						})
					})

					Context("when an interval is specified", func() {
						BeforeEach(func() {
							request.Source.Interval = "1m"
						})

						Context("when no version is given", func() {
							It("outputs a version containing the current time", func() {
								Expect(response).To(HaveLen(1))
								Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
							})
						})

						Context("when a version is given", func() {
							Context("with its time within the interval", func() {
								BeforeEach(func() {
									request.Version.Time = time.Now()
								})

								It("outputs a supplied version", func() {
									Expect(response).To(HaveLen(1))
									Expect(response[0].Time).To(Equal(request.Version.Time))
								})
							})

							Context("with its time one interval ago", func() {
								BeforeEach(func() {
									request.Version.Time = time.Now().Add(-1 * time.Minute)
								})

								It("outputs a version containing the current time and supplied version", func() {
									Expect(response).To(HaveLen(2))
									Expect(response[0].Time).To(Equal(request.Version.Time))
									Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
								})
							})

							Context("with its time N intervals ago", func() {
								BeforeEach(func() {
									request.Version.Time = time.Now().Add(-5 * time.Minute)
								})

								It("outputs a version containing the current time and supplied version", func() {
									Expect(response).To(HaveLen(2))
									Expect(response[0].Time).To(Equal(request.Version.Time))
									Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
								})
							})
						})
					})
				})

				Context("when the current day is specified", func() {
					BeforeEach(func() {
						request.Source.Days = []string{
							now.Add(24 * time.Hour).Weekday().String(),
							now.Add(48 * time.Hour).Weekday().String(),
						}
						request.Source.Days = []string{
							now.Weekday().String(),
							now.Add(48 * time.Hour).Weekday().String()}
					})

					It("outputs a version containing the current time", func() {
						Expect(response).To(HaveLen(1))
						Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})

				Context("when we are out of the specified day", func() {
					BeforeEach(func() {
						request.Source.Days = []string{
							now.Add(24 * time.Hour).Weekday().String(),
							now.Add(48 * time.Hour).Weekday().String(),
						}
					})

					It("does not output any versions", func() {
						Expect(response).To(BeEmpty())
					})
				})
			})

			Context("when we out of the specified time range", func() {
				BeforeEach(func() {
					start := now.Add(6 * time.Hour)
					stop := now.Add(7 * time.Hour)
					timeLayout := "3:04 PM -0700"

					request.Source.Start = start.Format(timeLayout)
					request.Source.Stop = stop.Format(timeLayout)
				})

				Context("when no version is given", func() {
					It("does not output any versions", func() {
						Expect(response).To(BeEmpty())
					})
				})

				Context("when an interval is given", func() {
					BeforeEach(func() {
						start := now.Add(6 * time.Hour)
						stop := now.Add(7 * time.Hour)
						timeLayout := "3:04 PM -0700"

						request.Source.Start = start.Format(timeLayout)
						request.Source.Stop = stop.Format(timeLayout)

						request.Source.Interval = "1m"
					})

					It("does not output any versions", func() {
						Expect(response).To(BeEmpty())
					})
				})
			})
		})

		Context("when an interval is specified", func() {
			BeforeEach(func() {
				request.Source.Interval = "1m"
			})

			Context("when no version is given", func() {
				It("outputs a version containing the current time", func() {
					Expect(response).To(HaveLen(1))
					Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
				})
			})

			Context("when a version is given", func() {
				Context("with its time within the interval", func() {
					BeforeEach(func() {
						request.Version.Time = time.Now()
					})

					It("outputs a supplied version", func() {
						Expect(response).To(HaveLen(1))
						Expect(response[0].Time).To(Equal(request.Version.Time))
					})
				})

				Context("with its time one interval ago", func() {
					BeforeEach(func() {
						request.Version.Time = time.Now().Add(-1 * time.Minute)
					})

					It("outputs a version containing the current time and supplied version", func() {
						Expect(response).To(HaveLen(2))
						Expect(response[0].Time).To(Equal(request.Version.Time))
						Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})

				Context("with its time N intervals ago", func() {
					BeforeEach(func() {
						request.Version.Time = time.Now().Add(-5 * time.Minute)
					})

					It("outputs a version containing the current time and supplied version", func() {
						Expect(response).To(HaveLen(2))
						Expect(response[0].Time).To(Equal(request.Version.Time))
						Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})
			})
		})
	})
})
<commit_msg>fix time zone handling in check tests<commit_after>package main_test

import (
	"encoding/json"
	"os/exec"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"

	. "github.com/concourse/time-resource/check"
	"github.com/concourse/time-resource/models"
)

var _ = Describe("Check", func() {
	var (
		checkCmd *exec.Cmd
		now      time.Time
	)

	BeforeEach(func() {
		now = time.Now().UTC()
	})

	Describe("ParseTime", func() {
		It("can parse many formats", func() {
			expectedTime := time.Date(0, 1, 1, 21, 0, 0, 0, time.UTC)

			formats := []string{
				"1:00 PM -0800",
				"1PM -0800",
				"1 PM -0800",
				"13:00 -0800",
				"1300 -0800",
			}

			for _, format := range formats {
				By("working with " + format)
				parsedTime, err := ParseTime(format)

				Expect(err).NotTo(HaveOccurred())
				Expect(parsedTime.Equal(expectedTime)).To(BeTrue())
			}
		})
	})

	Describe("ParseWeekday", func() {
		It("can parse a weekday", func() {
			parsedWeekdays, err := ParseWeekdays([]string{"Monday", "Tuesday"})

			Expect(err).NotTo(HaveOccurred())
			Expect(parsedWeekdays).To(Equal([]time.Weekday{time.Monday, time.Tuesday}))
		})

		It("raise error if weekday can't be parsed", func() {
			_, err := ParseWeekdays([]string{"Foo", "Tuesday"})

			Expect(err).To(HaveOccurred())
		})
	})

	Describe("IsInDays", func() {
		It("returns true if current day is in dayslist", func() {
			daysList := []time.Weekday{
				now.Weekday(),
				now.Add(24 * time.Hour).Weekday(),
			}

			Expect(IsInDays(now, daysList)).To(BeTrue())
		})

		It("return true if list is empty", func() {
			Expect(IsInDays(now, nil)).To(BeTrue())
		})

		It("returns false if not in list", func() {
			daysList := []time.Weekday{
				now.Add(24 * time.Hour).Weekday(),
				now.Add(48 * time.Hour).Weekday(),
			}

			Expect(IsInDays(now, daysList)).To(BeFalse())
		})
	})

	BeforeEach(func() {
		checkCmd = exec.Command(checkPath)
	})

	Context("with invalid inputs", func() {
		var request models.CheckRequest
		var response models.CheckResponse
		var session *gexec.Session

		BeforeEach(func() {
			request = models.CheckRequest{}
			response = models.CheckResponse{}
		})

		JustBeforeEach(func() {
			var err error

			stdin, err := checkCmd.StdinPipe()
			Expect(err).NotTo(HaveOccurred())

			session, err = gexec.Start(checkCmd, GinkgoWriter, GinkgoWriter)
			Expect(err).NotTo(HaveOccurred())

			err = json.NewEncoder(stdin).Encode(request)
			Expect(err).NotTo(HaveOccurred())
		})

		Context("with a missing everything", func() {
			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("one of 'interval' or 'between' must be specified"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid start", func() {
			BeforeEach(func() {
				request.Source.Start = "not-a-time"
				request.Source.Stop = "3:04 PM -0700"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid start time"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid stop", func() {
			BeforeEach(func() {
				request.Source.Start = "3:04 PM -0700"
				request.Source.Stop = "not-a-time"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid stop time"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with a missing stop", func() {
			BeforeEach(func() {
				request.Source.Start = "3:04 PM -0700"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("empty stop time!"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with a missing start", func() {
			BeforeEach(func() {
				request.Source.Stop = "3:04 PM -0700"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("empty start time!"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid interval ", func() {
			BeforeEach(func() {
				request.Source.Interval = "not-an-interval"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid interval"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})

		Context("with an invalid day ", func() {
			BeforeEach(func() {
				request.Source.Days = []string{"Foo", "Bar"}
				request.Source.Interval = "1m"
			})

			It("returns an error", func() {
				Eventually(session.Err).Should(gbytes.Say("invalid day 'Foo'"))
				Eventually(session).Should(gexec.Exit(1))
			})
		})
	})

	Context("when executed", func() {
		var request models.CheckRequest
		var response models.CheckResponse

		BeforeEach(func() {
			request = models.CheckRequest{}
			response = models.CheckResponse{}
		})

		JustBeforeEach(func() {
			stdin, err := checkCmd.StdinPipe()
			Expect(err).NotTo(HaveOccurred())

			session, err := gexec.Start(checkCmd, GinkgoWriter, GinkgoWriter)
			Expect(err).NotTo(HaveOccurred())

			err = json.NewEncoder(stdin).Encode(request)
			Expect(err).NotTo(HaveOccurred())

			Eventually(session).Should(gexec.Exit(0))

			err = json.Unmarshal(session.Out.Contents(), &response)
			Expect(err).NotTo(HaveOccurred())
		})

		Context("when a time range is specified", func() {
			Context("when we are in the specified time range", func() {
				BeforeEach(func() {
					start := now.Add(-1 * time.Hour)
					stop := now.Add(1 * time.Hour)
					timeLayout := "3:04 PM -0700"

					request.Source.Start = start.Format(timeLayout)
					request.Source.Stop = stop.Format(timeLayout)
				})

				Context("when no version is given", func() {
					It("outputs a version containing the current time", func() {
						Expect(response).To(HaveLen(1))
						Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})

				Context("when a version is given", func() {
					Context("when the resource has already triggered with in the current time range", func() {
						BeforeEach(func() {
							request.Version.Time = now.Add(-30 * time.Minute)
						})

						It("outputs a supplied version", func() {
							Expect(response).To(HaveLen(1))
							Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
						})
					})

					Context("when the resource was triggered yesterday near the end of the time frame", func() {
						BeforeEach(func() {
							request.Version.Time = now.Add(-23 * time.Hour)
						})

						It("outputs a version containing the current time and supplied version", func() {
							Expect(response).To(HaveLen(2))
							Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
							Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
						})
					})

					Context("when the resource was triggered yesterday in the current time frame", func() {
						BeforeEach(func() {
							request.Version.Time = now.Add(-24 * time.Hour)
						})

						It("outputs a version containing the current time and supplied version", func() {
							Expect(response).To(HaveLen(2))
							Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
							Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
						})
					})

					Context("when an interval is specified", func() {
						BeforeEach(func() {
							request.Source.Interval = "1m"
						})

						Context("when no version is given", func() {
							It("outputs a version containing the current time", func() {
								Expect(response).To(HaveLen(1))
								Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
							})
						})

						Context("when a version is given", func() {
							Context("with its time within the interval", func() {
								BeforeEach(func() {
									request.Version.Time = time.Now()
								})

								It("outputs a supplied version", func() {
									Expect(response).To(HaveLen(1))
									Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
								})
							})

							Context("with its time one interval ago", func() {
								BeforeEach(func() {
									request.Version.Time = time.Now().Add(-1 * time.Minute)
								})

								It("outputs a version containing the current time and supplied version", func() {
									Expect(response).To(HaveLen(2))
									Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
									Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
								})
							})

							Context("with its time N intervals ago", func() {
								BeforeEach(func() {
									request.Version.Time = time.Now().Add(-5 * time.Minute)
								})

								It("outputs a version containing the current time and supplied version", func() {
									Expect(response).To(HaveLen(2))
									Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
									Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
								})
							})
						})
					})
				})

				Context("when the current day is specified", func() {
					BeforeEach(func() {
						request.Source.Days = []string{
							now.Add(24 * time.Hour).Weekday().String(),
							now.Add(48 * time.Hour).Weekday().String(),
						}
						request.Source.Days = []string{
							now.Weekday().String(),
							now.Add(48 * time.Hour).Weekday().String()}
					})

					It("outputs a version containing the current time", func() {
						Expect(response).To(HaveLen(1))
						Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})

				Context("when we are out of the specified day", func() {
					BeforeEach(func() {
						request.Source.Days = []string{
							now.Add(24 * time.Hour).Weekday().String(),
							now.Add(48 * time.Hour).Weekday().String(),
						}
					})

					It("does not output any versions", func() {
						Expect(response).To(BeEmpty())
					})
				})
			})

			Context("when we out of the specified time range", func() {
				BeforeEach(func() {
					start := now.Add(6 * time.Hour)
					stop := now.Add(7 * time.Hour)
					timeLayout := "3:04 PM -0700"

					request.Source.Start = start.Format(timeLayout)
					request.Source.Stop = stop.Format(timeLayout)
				})

				Context("when no version is given", func() {
					It("does not output any versions", func() {
						Expect(response).To(BeEmpty())
					})
				})

				Context("when an interval is given", func() {
					BeforeEach(func() {
						start := now.Add(6 * time.Hour)
						stop := now.Add(7 * time.Hour)
						timeLayout := "3:04 PM -0700"

						request.Source.Start = start.Format(timeLayout)
						request.Source.Stop = stop.Format(timeLayout)

						request.Source.Interval = "1m"
					})

					It("does not output any versions", func() {
						Expect(response).To(BeEmpty())
					})
				})
			})
		})

		Context("when an interval is specified", func() {
			BeforeEach(func() {
				request.Source.Interval = "1m"
			})

			Context("when no version is given", func() {
				It("outputs a version containing the current time", func() {
					Expect(response).To(HaveLen(1))
					Expect(response[0].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
				})
			})

			Context("when a version is given", func() {
				Context("with its time within the interval", func() {
					BeforeEach(func() {
						request.Version.Time = time.Now()
					})

					It("outputs a supplied version", func() {
						Expect(response).To(HaveLen(1))
						Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
					})
				})

				Context("with its time one interval ago", func() {
					BeforeEach(func() {
						request.Version.Time = time.Now().Add(-1 * time.Minute)
					})

					It("outputs a version containing the current time and supplied version", func() {
						Expect(response).To(HaveLen(2))
						Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
						Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})

				Context("with its time N intervals ago", func() {
					BeforeEach(func() {
						request.Version.Time = time.Now().Add(-5 * time.Minute)
					})

					It("outputs a version containing the current time and supplied version", func() {
						Expect(response).To(HaveLen(2))
						Expect(response[0].Time.Unix()).To(BeNumerically("~", request.Version.Time.Unix(), 1))
						Expect(response[1].Time.Unix()).To(BeNumerically("~", time.Now().Unix(), 1))
					})
				})
			})
		})
	})
})
<|endoftext|>